Преглед изворни кода

Merge commit 'refs/pull/560/head' of https://gitee.com/zuohuaijun/Admin.NET into next

zuohuaijun пре 3 година
родитељ
комит
805c0507eb

+ 1 - 0
Admin.NET/Admin.NET.Core/Admin.NET.Core.csproj

@@ -17,6 +17,7 @@
     <PackageReference Include="AngleSharp" Version="1.0.1" />
     <PackageReference Include="AspectCore.Extensions.Reflection" Version="2.3.0" />
     <PackageReference Include="AspNetCoreRateLimit" Version="5.0.0" />
+    <PackageReference Include="DotNetCore.Compile.Environment" Version="3.2.0" />
     <PackageReference Include="DotNetCore.Natasha.CSharp" Version="5.0.2" />
     <PackageReference Include="FluentEmail.Smtp" Version="3.0.2" />
     <PackageReference Include="Furion.Extras.Authentication.JwtBearer" Version="4.8.6.4" />

+ 37 - 0
Admin.NET/Admin.NET.Core/Admin.NET.Core.xml

@@ -747,6 +747,11 @@
             更新时间
             </summary>
         </member>
+        <member name="P:Admin.NET.Core.SysJobDetail.CreateFromScript">
+            <summary>
+            是否为脚本创建的作业
+            </summary>
+        </member>
         <member name="P:Admin.NET.Core.SysJobDetail.ScriptCode">
             <summary>
             脚本代码
@@ -2672,6 +2677,26 @@
             已有相同编码或名称
             </summary>
         </member>
+        <member name="F:Admin.NET.Core.ErrorCodeEnum.D1701">
+            <summary>
+            脚本代码不能为空
+            </summary>
+        </member>
+        <member name="F:Admin.NET.Core.ErrorCodeEnum.D1702">
+            <summary>
+            脚本代码中的作业类,需要定义 [JobDetail] 特性
+            </summary>
+        </member>
+        <member name="F:Admin.NET.Core.ErrorCodeEnum.D1703">
+            <summary>
+            作业编号需要与脚本代码中的作业类 [JobDetail('jobId')] 一致
+            </summary>
+        </member>
+        <member name="F:Admin.NET.Core.ErrorCodeEnum.D1704">
+            <summary>
+            禁止修改作业编号
+            </summary>
+        </member>
         <member name="F:Admin.NET.Core.ErrorCodeEnum.xg1000">
             <summary>
             已存在同名或同编码项目
@@ -3544,6 +3569,18 @@
             <param name="message"></param>
             <returns></returns>
         </member>
+        <member name="T:Admin.NET.Core.DynamicJobCompiler">
+            <summary>
+            动态作业编译
+            </summary>
+        </member>
+        <member name="M:Admin.NET.Core.DynamicJobCompiler.BuildJob(System.String)">
+            <summary>
+            编译代码并返回其中实现 IJob 的类型
+            </summary>
+            <param name="script">动态编译的作业代码</param>
+            <returns></returns>
+        </member>
         <member name="T:Admin.NET.Core.LogJob">
             <summary>
             清理日志作业任务

+ 6 - 0
Admin.NET/Admin.NET.Core/Entity/SysJobDetail.cs

@@ -66,6 +66,12 @@ public class SysJobDetail : EntityBaseId
     [SugarColumn(ColumnDescription = "更新时间")]
     public DateTime? UpdatedTime { get; set; }
 
+    /// <summary>
+    /// 是否为脚本创建的作业
+    /// </summary>
+    [SugarColumn(ColumnDescription = "是否为脚本创建的作业")]
+    public bool CreateFromScript { get; set; } = false;
+
     /// <summary>
     /// 脚本代码
     /// </summary>

+ 1 - 1
Admin.NET/Admin.NET.Core/Entity/SysJobTrigger.cs

@@ -130,7 +130,7 @@ public class SysJobTrigger : EntityBaseId
     /// <summary>
     /// 是否在启动时重置最大触发次数等于一次的作业
     /// </summary>
-    [SugarColumn(ColumnDescription = "是否只运行一次")]
+    [SugarColumn(ColumnDescription = "是否重置触发次数")]
     public bool ResetOnlyOnce { get; set; } = true;
 
     /// <summary>

+ 24 - 0
Admin.NET/Admin.NET.Core/Enum/ErrorCodeEnum.cs

@@ -474,6 +474,30 @@ public enum ErrorCodeEnum
     [ErrorCodeItemMetadata("已有相同编码或名称")]
     D1602,
 
+    /// <summary>
+    /// 脚本代码不能为空
+    /// </summary>
+    [ErrorCodeItemMetadata("脚本代码不能为空")]
+    D1701,
+
+    /// <summary>
+    /// 脚本代码中的作业类,需要定义 [JobDetail] 特性
+    /// </summary>
+    [ErrorCodeItemMetadata("脚本代码中的作业类,需要定义 [JobDetail] 特性")]
+    D1702,
+
+    /// <summary>
+    /// 作业编号需要与脚本代码中的作业类 [JobDetail('jobId')] 一致
+    /// </summary>
+    [ErrorCodeItemMetadata("作业编号需要与脚本代码中的作业类 [JobDetail('jobId')] 一致")]
+    D1703,
+
+    /// <summary>
+    /// 禁止修改作业编号
+    /// </summary>
+    [ErrorCodeItemMetadata("禁止修改作业编号")]
+    D1704,
+
     /// <summary>
     /// 已存在同名或同编码项目
     /// </summary>

+ 27 - 0
Admin.NET/Admin.NET.Core/Job/DynamicJobCompiler.cs

@@ -0,0 +1,27 @@
+namespace Admin.NET.Core;
+
+/// <summary>
+/// 动态作业编译
+/// </summary>
+public class DynamicJobCompiler : ISingleton
+{
+    /// <summary>
+    /// 编译代码并返回其中实现 IJob 的类型
+    /// </summary>
+    /// <param name="script">动态编译的作业代码</param>
+    /// <returns></returns>
+    public Type BuildJob(string script)
+    {
+        //动态创建作业
+        var builder = new AssemblyCSharpBuilder("Admin.NET.Core")
+        {
+            Domain = DomainManagement.Random()
+        };
+
+        builder.Add(script);
+
+        var assembly = builder.GetAssembly();
+        var jobType = assembly.GetTypes().FirstOrDefault(u => typeof(IJob).IsAssignableFrom(u));
+        return jobType;
+    }
+}

+ 18 - 2
Admin.NET/Admin.NET.Core/Service/Job/DbJobPersistence.cs

@@ -21,6 +21,7 @@ public class DbJobPersistence : IJobPersistence
         using var serviceScope = _serviceProvider.CreateScope();
         var jobDetailRep = serviceScope.ServiceProvider.GetService<SqlSugarRepository<SysJobDetail>>();
         var jobTriggerRep = serviceScope.ServiceProvider.GetService<SqlSugarRepository<SysJobTrigger>>();
+        var dynamicJobCompiler = serviceScope.ServiceProvider.GetService<DynamicJobCompiler>();
 
         // 获取内存的作业
         IEnumerable<SchedulerBuilder> memoryJobs = App.EffectiveTypes.ScanToBuilders();
@@ -34,7 +35,20 @@ public class DbJobPersistence : IJobPersistence
         var dbJobs = jobDetailRep.GetList();
         foreach (var dbJob in dbJobs)
         {
-            var jobDetail = JobBuilder.Create(dbJob.AssemblyName, dbJob.JobType).LoadFrom(dbJob);
+            JobBuilder jobDetail;
+            if (dbJob.CreateFromScript)
+            {
+                // 动态创建作业
+                var jobType = dynamicJobCompiler.BuildJob(dbJob.ScriptCode);
+                jobDetail = JobBuilder.Create(jobType).LoadFrom(dbJob);
+            }
+            else
+            {
+                jobDetail = JobBuilder.Create(dbJob.AssemblyName, dbJob.JobType).LoadFrom(dbJob);
+            }
+
+            //强行设置为不扫描 IJob 实现类 [Trigger] 特性触发器,否则 SchedulerBuilder.Create 会再次扫描,导致重复添加同名触发器
+            jobDetail.SetIncludeAnnotations(false);
 
             // 加载数据库的触发器
             var triggerBuilders = new List<TriggerBuilder>();
@@ -105,7 +119,8 @@ public class DbJobPersistence : IJobPersistence
         }
         else if (context.Behavior == PersistenceBehavior.Updated)
         {
-            db.Updateable(jobDetail).WhereColumns(u => new { u.JobId }).IgnoreColumns(u => new { u.Id }).ExecuteCommand();
+            //忽略 Id 和额外自定义的列
+            db.Updateable(jobDetail).WhereColumns(u => new { u.JobId }).IgnoreColumns(u => new { u.Id, u.CreateFromScript, u.ScriptCode }).ExecuteCommand();
         }
         else if (context.Behavior == PersistenceBehavior.Removed)
         {
@@ -129,6 +144,7 @@ public class DbJobPersistence : IJobPersistence
         }
         else if (context.Behavior == PersistenceBehavior.Updated)
         {
+            //忽略 Id 和额外自定义的列
             db.Updateable(jobTrigger).WhereColumns(u => new { u.TriggerId, u.JobId }).IgnoreColumns(u => new { u.Id }).ExecuteCommand();
         }
         else if (context.Behavior == PersistenceBehavior.Removed)

+ 80 - 22
Admin.NET/Admin.NET.Core/Service/Job/SysJobService.cs

@@ -10,16 +10,19 @@ public class SysJobService : IDynamicApiController, ITransient
     private readonly SqlSugarRepository<SysJobTrigger> _sysJobTriggerRep;
     private readonly SqlSugarRepository<SysJobCluster> _sysJobClusterRep;
     private readonly ISchedulerFactory _schedulerFactory;
+    private readonly DynamicJobCompiler _dynamicJobCompiler;
 
     public SysJobService(SqlSugarRepository<SysJobDetail> sysJobDetailRep,
         SqlSugarRepository<SysJobTrigger> sysJobTriggerRep,
         SqlSugarRepository<SysJobCluster> sysJobClusterRep,
-        ISchedulerFactory schedulerFactory)
+        ISchedulerFactory schedulerFactory,
+        DynamicJobCompiler dynamicJobCompiler)
     {
         _sysJobDetailRep = sysJobDetailRep;
         _sysJobTriggerRep = sysJobTriggerRep;
         _sysJobClusterRep = sysJobClusterRep;
         _schedulerFactory = schedulerFactory;
+        _dynamicJobCompiler = dynamicJobCompiler;
     }
 
     /// <summary>
@@ -47,7 +50,7 @@ public class SysJobService : IDynamicApiController, ITransient
         {
             foreach (var jobTrigger in job.JobTriggers)
             {
-                jobTrigger.Args = rgx.Match(jobTrigger.Args).Value;
+                jobTrigger.Args = rgx.Match(jobTrigger.Args ?? "").Value;
             }
         }
         return jobDetails;
@@ -65,15 +68,27 @@ public class SysJobService : IDynamicApiController, ITransient
         if (isExist)
             throw Oops.Oh(ErrorCodeEnum.D1006);
 
+        if (string.IsNullOrEmpty(input.ScriptCode))
+            throw Oops.Oh(ErrorCodeEnum.D1701);
+
+        input.CreateFromScript = true;//确保为true
         // 动态创建作业
-        NatashaInitializer.Preheating();
-        var oop = new AssemblyCSharpBuilder("Admin.NET.Core");
-        oop.Domain = DomainManagement.Random();
-        oop.Add(input.ScriptCode);
-        var jobType = oop.GetTypeFromShortName(input.JobId);
-        _schedulerFactory.AddJob(JobBuilder.Create(jobType).SetIncludeAnnotations(input.IncludeAnnotations));
-
-        await _sysJobDetailRep.InsertAsync(input.Adapt<SysJobDetail>());
+        var jobType = _dynamicJobCompiler.BuildJob(input.ScriptCode);
+
+        if (jobType.GetCustomAttributes(typeof(JobDetailAttribute)).FirstOrDefault() is not JobDetailAttribute jobDetailAttribute)
+            throw Oops.Oh(ErrorCodeEnum.D1702);
+        if (jobDetailAttribute.JobId != input.JobId)
+            throw Oops.Oh(ErrorCodeEnum.D1703);
+
+        _schedulerFactory.AddJob(
+            JobBuilder.Create(jobType).SetIncludeAnnotations(input.IncludeAnnotations)
+                .LoadFrom(input.Adapt<SysJobDetail>()).SetJobType(jobType));
+
+        //延迟一下等待持久化写入,再执行其他字段的更新
+        await Task.Delay(500);
+        await _sysJobDetailRep.AsUpdateable()
+            .SetColumns(u => new SysJobDetail { CreateFromScript = input.CreateFromScript, ScriptCode = input.ScriptCode })
+            .Where(u => u.JobId == input.JobId).ExecuteCommandAsync();
     }
 
     /// <summary>
@@ -88,7 +103,41 @@ public class SysJobService : IDynamicApiController, ITransient
         if (isExist)
             throw Oops.Oh(ErrorCodeEnum.D1006);
 
-        await _sysJobDetailRep.UpdateAsync(input.Adapt<SysJobDetail>());
+        var sysJobDetail = await _sysJobDetailRep.GetFirstAsync(u => u.Id == input.Id);
+        if (sysJobDetail.JobId != input.JobId)
+            throw Oops.Oh(ErrorCodeEnum.D1704);
+
+        var scheduler = _schedulerFactory.GetJob(sysJobDetail.JobId);
+        var oldScriptCode = sysJobDetail.ScriptCode;//旧脚本代码
+        input.Adapt(sysJobDetail);
+
+        if (input.CreateFromScript)
+        {
+            if (string.IsNullOrEmpty(input.ScriptCode))
+                throw Oops.Oh(ErrorCodeEnum.D1701);
+
+            if (input.ScriptCode != oldScriptCode)
+            {
+                // 动态创建作业
+                var jobType = _dynamicJobCompiler.BuildJob(input.ScriptCode);
+
+                if (jobType.GetCustomAttributes(typeof(JobDetailAttribute)).FirstOrDefault() is not JobDetailAttribute jobDetailAttribute)
+                    throw Oops.Oh(ErrorCodeEnum.D1702);
+                if (jobDetailAttribute.JobId != input.JobId)
+                    throw Oops.Oh(ErrorCodeEnum.D1703);
+
+                scheduler?.UpdateDetail(JobBuilder.Create(jobType).LoadFrom(sysJobDetail).SetJobType(jobType));
+            }
+        }
+        else
+        {
+            scheduler?.UpdateDetail(scheduler.GetJobBuilder().LoadFrom(sysJobDetail));
+        }
+
+        //tip: 假如这次更新有变更了 JobId,变更 JobId 后触发的持久化更新执行,会由于找不到 JobId 而更新不到数据
+        //延迟一下等待持久化写入,再执行其他字段的更新
+        await Task.Delay(500);
+        await _sysJobDetailRep.UpdateAsync(sysJobDetail);
     }
 
     /// <summary>
@@ -101,6 +150,7 @@ public class SysJobService : IDynamicApiController, ITransient
     {
         _schedulerFactory.RemoveJob(input.JobId);
 
+        //如果 _schedulerFactory 中不存在 JodId,则无法触发持久化,下面的代码确保作业和触发器能被删除
         await _sysJobDetailRep.DeleteAsync(u => u.JobId == input.JobId);
         await _sysJobTriggerRep.DeleteAsync(u => u.JobId == input.JobId);
     }
@@ -131,7 +181,9 @@ public class SysJobService : IDynamicApiController, ITransient
 
         var jobTrigger = input.Adapt<SysJobTrigger>();
         jobTrigger.Args = "[" + jobTrigger.Args + "]";
-        await _sysJobTriggerRep.InsertAsync(jobTrigger);
+
+        var scheduler = _schedulerFactory.GetJob(input.JobId);
+        scheduler?.AddTrigger(Triggers.Create(input.AssemblyName, input.TriggerType).LoadFrom(jobTrigger));
     }
 
     /// <summary>
@@ -148,7 +200,9 @@ public class SysJobService : IDynamicApiController, ITransient
 
         var jobTrigger = input.Adapt<SysJobTrigger>();
         jobTrigger.Args = "[" + jobTrigger.Args + "]";
-        await _sysJobTriggerRep.UpdateAsync(jobTrigger);
+
+        var scheduler = _schedulerFactory.GetJob(input.JobId);
+        scheduler?.UpdateTrigger(Triggers.Create(input.AssemblyName, input.TriggerType).LoadFrom(jobTrigger));
     }
 
     /// <summary>
@@ -159,7 +213,11 @@ public class SysJobService : IDynamicApiController, ITransient
     [DisplayName("删除触发器")]
     public async Task DeleteJobTrigger(DeleteJobTriggerInput input)
     {
-        await _sysJobTriggerRep.DeleteAsync(u => u.TriggerId == input.TriggerId);
+        var scheduler = _schedulerFactory.GetJob(input.JobId);
+        scheduler?.RemoveTrigger(input.TriggerId);
+
+        //如果 _schedulerFactory 中不存在 JodId,则无法触发持久化,下行代码确保触发器能被删除
+        await _sysJobTriggerRep.DeleteAsync(u => u.JobId == input.JobId && u.TriggerId == input.TriggerId);
     }
 
     /// <summary>
@@ -191,8 +249,8 @@ public class SysJobService : IDynamicApiController, ITransient
     [DisplayName("暂停作业")]
     public void PauseJob(JobDetailInput input)
     {
-        _ = _schedulerFactory.TryGetJob(input.JobId, out var _scheduler);
-        _scheduler?.Pause();
+        var scheduler = _schedulerFactory.GetJob(input.JobId);
+        scheduler?.Pause();
     }
 
     /// <summary>
@@ -202,8 +260,8 @@ public class SysJobService : IDynamicApiController, ITransient
     [DisplayName("启动作业")]
     public void StartJob(JobDetailInput input)
     {
-        _ = _schedulerFactory.TryGetJob(input.JobId, out var _scheduler);
-        _scheduler?.Start();
+        var scheduler = _schedulerFactory.GetJob(input.JobId);
+        scheduler?.Start();
     }
 
     /// <summary>
@@ -213,8 +271,8 @@ public class SysJobService : IDynamicApiController, ITransient
     [DisplayName("暂停触发器")]
     public void PauseTrigger(JobTriggerInput input)
     {
-        _ = _schedulerFactory.TryGetJob(input.JobId, out var _scheduler);
-        _scheduler?.PauseTrigger(input.TriggerId);
+        var scheduler = _schedulerFactory.GetJob(input.JobId);
+        scheduler?.PauseTrigger(input.TriggerId);
     }
 
     /// <summary>
@@ -224,8 +282,8 @@ public class SysJobService : IDynamicApiController, ITransient
     [DisplayName("启动触发器")]
     public void StartTrigger(JobTriggerInput input)
     {
-        _ = _schedulerFactory.TryGetJob(input.JobId, out var _scheduler);
-        _scheduler?.StartTrigger(input.TriggerId);
+        var scheduler = _schedulerFactory.GetJob(input.JobId);
+        scheduler?.StartTrigger(input.TriggerId);
     }
 
     /// <summary>

+ 3 - 1
Admin.NET/Admin.NET.Web.Core/Startup.cs

@@ -10,7 +10,6 @@ using Microsoft.AspNetCore.HttpOverrides;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.Hosting;
 using Microsoft.Extensions.Logging;
-using NewLife.Model;
 using Newtonsoft.Json;
 using Newtonsoft.Json.Serialization;
 using OnceMi.AspNetCore.OSS;
@@ -189,6 +188,9 @@ public class Startup : AppStartup
 
         // 验证码
         services.AddLazyCaptcha();
+
+        // 动态编译预热
+        NatashaInitializer.Preheating();
     }
 
     public void Configure(IApplicationBuilder app, IWebHostEnvironment env)

+ 2 - 1
Web/src/api-services/api.ts

@@ -10,7 +10,8 @@
  * NOTE: This class is auto generated by the swagger code generator program.
  * https://github.com/swagger-api/swagger-codegen.git
  * Do not edit the class manually.
- */export * from './apis/sys-auth-api';
+ */export * from './apis/apijsonapi';
+export * from './apis/sys-auth-api';
 export * from './apis/sys-cache-api';
 export * from './apis/sys-code-gen-api';
 export * from './apis/sys-code-gen-config-api';

+ 131 - 0
Web/src/api-services/apis/apijsonapi.ts

@@ -0,0 +1,131 @@
+/* tslint:disable */
+/* eslint-disable */
+/**
+ * Admin.NET
+ * 让 .NET 开发更简单、更通用、更流行。前后端分离架构(.NET6/Vue3),开箱即用紧随前沿技术。<br/><a href='https://gitee.com/zuohuaijun/Admin.NET/'>https://gitee.com/zuohuaijun/Admin.NET</a>
+ *
+ * OpenAPI spec version: 1.0.0
+ * Contact: 515096995@qq.com
+ *
+ * NOTE: This class is auto generated by the swagger code generator program.
+ * https://github.com/swagger-api/swagger-codegen.git
+ * Do not edit the class manually.
+ */
+import globalAxios, { AxiosResponse, AxiosInstance, AxiosRequestConfig } from 'axios';
+import { Configuration } from '../configuration';
+// Some imports not used depending on template conditions
+// @ts-ignore
+import { BASE_PATH, COLLECTION_FORMATS, RequestArgs, BaseAPI, RequiredError } from '../base';
+import { AdminResultJObject } from '../models';
+import { JToken } from '../models';
+/**
+ * APIJSONApi - axios parameter creator
+ * @export
+ */
+export const APIJSONApiAxiosParamCreator = function (configuration?: Configuration) {
+    return {
+        /**
+         * 
+         * @summary 统一入口
+         * @param {{ [key: string]: JToken; }} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        apiAPIJSONPost: async (body?: { [key: string]: JToken; }, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/api/aPIJSON`;
+            // use dummy base URL string because the URL constructor only accepts absolute URLs.
+            const localVarUrlObj = new URL(localVarPath, 'https://example.com');
+            let baseOptions;
+            if (configuration) {
+                baseOptions = configuration.baseOptions;
+            }
+            const localVarRequestOptions :AxiosRequestConfig = { method: 'POST', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+            // authentication Bearer required
+
+            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';
+
+            const query = new URLSearchParams(localVarUrlObj.search);
+            for (const key in localVarQueryParameter) {
+                query.set(key, localVarQueryParameter[key]);
+            }
+            for (const key in options.params) {
+                query.set(key, options.params[key]);
+            }
+            localVarUrlObj.search = (new URLSearchParams(query)).toString();
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+            const needsSerialization = (typeof body !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json';
+            localVarRequestOptions.data =  needsSerialization ? JSON.stringify(body !== undefined ? body : {}) : (body || "");
+
+            return {
+                url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash,
+                options: localVarRequestOptions,
+            };
+        },
+    }
+};
+
+/**
+ * APIJSONApi - functional programming interface
+ * @export
+ */
+export const APIJSONApiFp = function(configuration?: Configuration) {
+    return {
+        /**
+         * 
+         * @summary 统一入口
+         * @param {{ [key: string]: JToken; }} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiAPIJSONPost(body?: { [key: string]: JToken; }, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<AdminResultJObject>>> {
+            const localVarAxiosArgs = await APIJSONApiAxiosParamCreator(configuration).apiAPIJSONPost(body, options);
+            return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
+                const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
+                return axios.request(axiosRequestArgs);
+            };
+        },
+    }
+};
+
+/**
+ * APIJSONApi - factory interface
+ * @export
+ */
+export const APIJSONApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
+    return {
+        /**
+         * 
+         * @summary 统一入口
+         * @param {{ [key: string]: JToken; }} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiAPIJSONPost(body?: { [key: string]: JToken; }, options?: AxiosRequestConfig): Promise<AxiosResponse<AdminResultJObject>> {
+            return APIJSONApiFp(configuration).apiAPIJSONPost(body, options).then((request) => request(axios, basePath));
+        },
+    };
+};
+
+/**
+ * APIJSONApi - object-oriented interface
+ * @export
+ * @class APIJSONApi
+ * @extends {BaseAPI}
+ */
+export class APIJSONApi extends BaseAPI {
+    /**
+     * 
+     * @summary 统一入口
+     * @param {{ [key: string]: JToken; }} [body] 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof APIJSONApi
+     */
+    public async apiAPIJSONPost(body?: { [key: string]: JToken; }, options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminResultJObject>> {
+        return APIJSONApiFp(this.configuration).apiAPIJSONPost(body, options).then((request) => request(this.axios, this.basePath));
+    }
+}

+ 6 - 0
Web/src/api-services/models/add-job-detail-input.ts

@@ -71,6 +71,12 @@ export interface AddJobDetailInput {
      * @memberof AddJobDetailInput
      */
     updatedTime?: Date | null;
+    /**
+     * 是否为脚本创建的作业
+     * @type {boolean}
+     * @memberof AddJobDetailInput
+     */
+    createFromScript?: boolean;
     /**
      * 脚本代码
      * @type {string}

+ 57 - 0
Web/src/api-services/models/admin-result-jobject.ts

@@ -0,0 +1,57 @@
+/* tslint:disable */
+/* eslint-disable */
+/**
+ * Admin.NET
+ * 让 .NET 开发更简单、更通用、更流行。前后端分离架构(.NET6/Vue3),开箱即用紧随前沿技术。<br/><a href='https://gitee.com/zuohuaijun/Admin.NET/'>https://gitee.com/zuohuaijun/Admin.NET</a>
+ *
+ * OpenAPI spec version: 1.0.0
+ * Contact: 515096995@qq.com
+ *
+ * NOTE: This class is auto generated by the swagger code generator program.
+ * https://github.com/swagger-api/swagger-codegen.git
+ * Do not edit the class manually.
+ */
+import { JToken } from './jtoken';
+/**
+ * 全局返回结果
+ * @export
+ * @interface AdminResultJObject
+ */
+export interface AdminResultJObject {
+    /**
+     * 状态码
+     * @type {number}
+     * @memberof AdminResultJObject
+     */
+    code?: number;
+    /**
+     * 类型success、warning、error
+     * @type {string}
+     * @memberof AdminResultJObject
+     */
+    type?: string | null;
+    /**
+     * 错误信息
+     * @type {string}
+     * @memberof AdminResultJObject
+     */
+    message?: string | null;
+    /**
+     * 数据
+     * @type {{ [key: string]: JToken; }}
+     * @memberof AdminResultJObject
+     */
+    result?: { [key: string]: JToken; } | null;
+    /**
+     * 附加数据
+     * @type {any}
+     * @memberof AdminResultJObject
+     */
+    extras?: any | null;
+    /**
+     * 时间
+     * @type {Date}
+     * @memberof AdminResultJObject
+     */
+    time?: Date;
+}

+ 2 - 0
Web/src/api-services/models/index.ts

@@ -18,6 +18,7 @@ export * from './admin-result-file-output';
 export * from './admin-result-iaction-result';
 export * from './admin-result-int32';
 export * from './admin-result-int64';
+export * from './admin-result-jobject';
 export * from './admin-result-list-code-gen-config';
 export * from './admin-result-list-column-ouput';
 export * from './admin-result-list-const-output';
@@ -113,6 +114,7 @@ export * from './file-output';
 export * from './gen-auth-url-input';
 export * from './gender-enum';
 export * from './iaction-result';
+export * from './jtoken';
 export * from './job-detail-input';
 export * from './job-output';
 export * from './job-trigger-input';

+ 21 - 0
Web/src/api-services/models/jtoken.ts

@@ -0,0 +1,21 @@
+/* tslint:disable */
+/* eslint-disable */
+/**
+ * Admin.NET
+ * 让 .NET 开发更简单、更通用、更流行。前后端分离架构(.NET6/Vue3),开箱即用紧随前沿技术。<br/><a href='https://gitee.com/zuohuaijun/Admin.NET/'>https://gitee.com/zuohuaijun/Admin.NET</a>
+ *
+ * OpenAPI spec version: 1.0.0
+ * Contact: 515096995@qq.com
+ *
+ * NOTE: This class is auto generated by the swagger code generator program.
+ * https://github.com/swagger-api/swagger-codegen.git
+ * Do not edit the class manually.
+ */
+import { JToken } from './jtoken';
+/**
+ * 
+ * @export
+ * @interface JToken
+ */
+export interface JToken extends Array<JToken> {
+}

+ 6 - 0
Web/src/api-services/models/sys-job-detail.ts

@@ -77,6 +77,12 @@ export interface SysJobDetail {
      * @memberof SysJobDetail
      */
     updatedTime?: Date | null;
+    /**
+     * 是否为脚本创建的作业
+     * @type {boolean}
+     * @memberof SysJobDetail
+     */
+    createFromScript?: boolean;
     /**
      * 脚本代码
      * @type {string}

+ 6 - 0
Web/src/api-services/models/update-job-detail-input.ts

@@ -71,6 +71,12 @@ export interface UpdateJobDetailInput {
      * @memberof UpdateJobDetailInput
      */
     updatedTime?: Date | null;
+    /**
+     * 是否为脚本创建的作业
+     * @type {boolean}
+     * @memberof UpdateJobDetailInput
+     */
+    createFromScript?: boolean;
     /**
      * 脚本代码
      * @type {string}

+ 31 - 8
Web/src/views/system/job/component/JobScriptCode.ts

@@ -1,15 +1,32 @@
-export const JobScriptCode = `namespace Admin.NET.Core;
+export const JobScriptCode = `#region using
+
+using Furion;
+using Furion.Logging;
+using Furion.RemoteRequest.Extensions;
+using Furion.Schedule;
+using Microsoft.Extensions.DependencyInjection;
+using System;
+using System.Data;
+using System.Linq.Dynamic.Core;
+using System.Linq.Expressions;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Yitter.IdGenerator;
+
+#endregion
+
+namespace Admin.NET.Core;
 
 /// <summary>
-/// XXX作业任务
+/// 动态作业任务
 /// </summary>
-[JobDetail("job_XXX", Description = "XXX", GroupName = "default", Concurrent = false)]
-[Daily(TriggerId = "trigger_XXX", Description = "XXX")]
-public class XXXJob : IJob
+[JobDetail("你的作业编号")]
+public class DynamicJob : IJob
 {
     private readonly IServiceProvider _serviceProvider;
 
-    public XXXJob(IServiceProvider serviceProvider)
+    public DynamicJob(IServiceProvider serviceProvider)
     {
         _serviceProvider = serviceProvider;
     }
@@ -17,9 +34,15 @@ public class XXXJob : IJob
     public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
     {
         using var serviceScope = _serviceProvider.CreateScope();
-        var db = serviceScope.ServiceProvider.GetService<ISqlSugarClient>();
+        
+        // 获取用户仓储
+        // var rep = serviceScope.ServiceProvider.GetService<SqlSugarRepository<SysUser>>();
 
-        自己的逻辑代码。。。
+        // 请求网址
+        // var result = await "http://www.baidu.com".GetAsStringAsync();
+        // Console.WriteLine(result);
 
+        // 日志
+        // Log.Information("日志消息");
     }
 }`;

+ 24 - 9
Web/src/views/system/job/component/editJobDetail.vue

@@ -7,13 +7,13 @@
 					<span> {{ props.title }} </span>
 				</div>
 			</template>
-			<el-tabs>
+			<el-tabs v-model="state.selectedTabName">
 				<el-tab-pane label="作业信息">
-					<el-form :model="state.ruleForm" ref="ruleFormRef" size="default" label-width="110px">
+					<el-form :model="state.ruleForm" ref="ruleFormRef" size="default" label-width="130px">
 						<el-row :gutter="35">
 							<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
 								<el-form-item label="作业编号" prop="jobId" :rules="[{ required: true, message: '作业编号不能为空', trigger: 'blur' }]">
-									<el-input v-model="state.ruleForm.jobId" placeholder="作业编号" clearable />
+									<el-input v-model="state.ruleForm.jobId" placeholder="作业编号" :disabled="isEdit" clearable />
 								</el-form-item>
 							</el-col>
 							<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
@@ -29,8 +29,16 @@
 									</el-radio-group>
 								</el-form-item>
 							</el-col>
-							<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
-								<el-form-item label="扫描特性触发器" prop="includeAnnotations">
+							<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20" v-show="!isEdit">
+								<el-form-item prop="includeAnnotations">
+									<template v-slot:label>
+										<div>
+											扫描特性触发器
+											<el-tooltip raw-content content="此参数只在新增作业时生效<br/>扫描定义在作业上的触发器" placement="top">
+												<SvgIcon name="fa fa-question-circle-o" :size="16" style="vertical-align: middle" />
+											</el-tooltip>
+										</div>
+									</template>
 									<el-radio-group v-model="state.ruleForm.includeAnnotations">
 										<el-radio :label="true">是</el-radio>
 										<el-radio :label="false">否</el-radio>
@@ -50,7 +58,7 @@
 						</el-row>
 					</el-form>
 				</el-tab-pane>
-				<el-tab-pane label="脚本代码">
+				<el-tab-pane label="脚本代码" :disabled="!state.ruleForm.createFromScript">
 					<div ref="monacoEditorRef" style="width: 100%; height: 500px"></div>
 				</el-tab-pane>
 			</el-tabs>
@@ -65,7 +73,7 @@
 </template>
 
 <script lang="ts" setup name="sysEditJobDetail">
-import { reactive, ref } from 'vue';
+import { reactive, ref, computed } from 'vue';
 import mittBus from '/@/utils/mitt';
 import * as monaco from 'monaco-editor';
 import { JobScriptCode } from './JobScriptCode';
@@ -82,16 +90,22 @@ const ruleFormRef = ref();
 const monacoEditorRef = ref();
 const state = reactive({
 	isShowDialog: false,
+	selectedTabName: '0', // 选中的 tab 页
 	ruleForm: {} as UpdateJobDetailInput,
 	monacoEditor: null as any,
 });
 
+// 是否编辑状态
+const isEdit = computed(() => {
+	return state.ruleForm.id != undefined && state.ruleForm.id > 0;
+});
+
 // 初始化monacoEditor对象
 var monacoEditor: any = null;
 const initMonacoEditor = () => {
 	monacoEditor = monaco.editor.create(monacoEditorRef.value, {
 		theme: 'vs-dark', // 主题 vs vs-dark hc-black
-		value: JobScriptCode, // 默认显示的值
+		value: '', // 默认显示的值
 		language: 'csharp',
 		formatOnPaste: true,
 		wordWrap: 'on', //自动换行,注意大小写
@@ -117,13 +131,14 @@ const initMonacoEditor = () => {
 
 // 打开弹窗
 const openDialog = (row: any) => {
+	state.selectedTabName = '0'; // 重置为第一个 tab 页
 	state.ruleForm = JSON.parse(JSON.stringify(row));
 	state.isShowDialog = true;
 
 	// 延迟拿值防止取不到
 	setTimeout(() => {
 		if (monacoEditor == null) initMonacoEditor();
-		else monacoEditor.setValue(row.id == undefined ? JobScriptCode : state.ruleForm.scriptCode);
+		monacoEditor.setValue(row.id == undefined ? JobScriptCode : state.ruleForm.scriptCode);
 	}, 1);
 };
 

+ 12 - 4
Web/src/views/system/job/component/editJobTrigger.vue

@@ -7,7 +7,7 @@
 					<span> {{ props.title }} </span>
 				</div>
 			</template>
-			<el-form :model="state.ruleForm" ref="ruleFormRef" size="default" label-width="110px">
+			<el-form :model="state.ruleForm" ref="ruleFormRef" size="default" label-width="130px">
 				<el-row :gutter="35">
 					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
 						<el-form-item label="触发器编号" prop="triggerId" :rules="[{ required: true, message: '触发器编号不能为空', trigger: 'blur' }]">
@@ -23,8 +23,8 @@
 						</el-form-item>
 					</el-col>
 					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20" v-if="state.ruleForm.triggerType == 'Furion.Schedule.PeriodTrigger'">
-						<el-form-item label="参数">
-							<el-input-number v-model="state.ruleForm.args" placeholder="间隔" class="w100" />
+						<el-form-item label="参数(ms)">
+							<el-input-number v-model="state.ruleForm.args" placeholder="间隔" :min="100" :step="100" class="w100" />
 						</el-form-item>
 					</el-col>
 					<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20" v-else>
@@ -83,7 +83,15 @@
 						</el-form-item>
 					</el-col>
 					<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
-						<el-form-item label="仅执行一次">
+						<el-form-item>
+							<template v-slot:label>
+								<div>
+									重置触发次数
+									<el-tooltip raw-content content="是否在启动时重置最大触发次数等于一次的作业<br/>解决因持久化数据已完成一次触发但启动时不再执行的问题" placement="top">
+										<SvgIcon name="fa fa-question-circle-o" :size="16" style="vertical-align: middle" />
+									</el-tooltip>
+								</div>
+							</template>
 							<el-radio-group v-model="state.ruleForm.resetOnlyOnce">
 								<el-radio :label="true">是</el-radio>
 								<el-radio :label="false">否</el-radio>

+ 30 - 24
Web/src/views/system/job/index.vue

@@ -49,19 +49,19 @@
 							<el-table-column prop="description" label="描述" width="120" show-overflow-tooltip />
 							<el-table-column prop="status" label="状态" width="100" align="center" show-overflow-tooltip>
 								<template #default="scope">
-									<el-tag v-if="scope.row.status == 0"> 积压 </el-tag>
-									<el-tag v-if="scope.row.status == 1"> 就绪 </el-tag>
-									<el-tag v-if="scope.row.status == 2"> 正在运行 </el-tag>
-									<el-tag v-if="scope.row.status == 3"> 暂停 </el-tag>
-									<el-tag v-if="scope.row.status == 4"> 阻塞 </el-tag>
-									<el-tag v-if="scope.row.status == 5"> 由失败进入就绪 </el-tag>
-									<el-tag v-if="scope.row.status == 6"> 归档 </el-tag>
-									<el-tag v-if="scope.row.status == 7"> 崩溃 </el-tag>
-									<el-tag v-if="scope.row.status == 8"> 超限 </el-tag>
-									<el-tag v-if="scope.row.status == 9"> 无触发时间 </el-tag>
-									<el-tag v-if="scope.row.status == 10"> 未启动 </el-tag>
-									<el-tag v-if="scope.row.status == 11"> 未知作业触发器 </el-tag>
-									<el-tag v-if="scope.row.status == 12"> 未知作业处理程序 </el-tag>
+									<el-tag type="warning" effect="plain" v-if="scope.row.status == 0"> 积压 </el-tag>
+									<el-tag type="" effect="plain" v-if="scope.row.status == 1"> 就绪 </el-tag>
+									<el-tag type="success" effect="plain" v-if="scope.row.status == 2"> 正在运行 </el-tag>
+									<el-tag type="danger" effect="plain" v-if="scope.row.status == 3"> 暂停 </el-tag>
+									<el-tag type="danger" effect="plain" v-if="scope.row.status == 4"> 阻塞 </el-tag>
+									<el-tag type="" effect="plain" v-if="scope.row.status == 5"> 由失败进入就绪 </el-tag>
+									<el-tag type="danger" effect="plain" v-if="scope.row.status == 6"> 归档 </el-tag>
+									<el-tag type="danger" effect="plain" v-if="scope.row.status == 7"> 崩溃 </el-tag>
+									<el-tag type="danger" effect="plain" v-if="scope.row.status == 8"> 超限 </el-tag>
+									<el-tag type="danger" effect="plain" v-if="scope.row.status == 9"> 无触发时间 </el-tag>
+									<el-tag type="danger" effect="plain" v-if="scope.row.status == 10"> 未启动 </el-tag>
+									<el-tag type="danger" effect="plain" v-if="scope.row.status == 11"> 未知作业触发器 </el-tag>
+									<el-tag type="danger" effect="plain" v-if="scope.row.status == 12"> 未知作业处理程序 </el-tag>
 								</template>
 							</el-table-column>
 							<el-table-column prop="startTime" label="起始时间" width="100" align="center" show-overflow-tooltip />
@@ -77,19 +77,19 @@
 							<el-table-column prop="startNow" label="是否立即启动" width="100" align="center" show-overflow-tooltip>
 								<template #default="scope">
 									<el-tag v-if="scope.row.startNow == true"> 是 </el-tag>
-									<el-tag v-else> 否 </el-tag>
+									<el-tag type="info" v-else> 否 </el-tag>
 								</template>
 							</el-table-column>
 							<el-table-column prop="runOnStart" label="是否启动时执行一次" width="150" align="center" show-overflow-tooltip>
 								<template #default="scope">
 									<el-tag v-if="scope.row.runOnStart == true"> 是 </el-tag>
-									<el-tag v-else> 否 </el-tag>
+									<el-tag type="info" v-else> 否 </el-tag>
 								</template>
 							</el-table-column>
-							<el-table-column prop="resetOnlyOnce" label="是否只运行一次" width="120" align="center" show-overflow-tooltip>
+							<el-table-column prop="resetOnlyOnce" label="是否重置触发次数" width="120" align="center" show-overflow-tooltip>
 								<template #default="scope">
 									<el-tag v-if="scope.row.resetOnlyOnce == true"> 是 </el-tag>
-									<el-tag v-else> 否 </el-tag>
+									<el-tag type="info" v-else> 否 </el-tag>
 								</template>
 							</el-table-column>
 							<el-table-column prop="updatedTime" label="更新时间" width="140" align="center" show-overflow-tooltip />
@@ -127,16 +127,22 @@
 				<el-table-column prop="jobDetail.description" label="描述" show-overflow-tooltip />
 				<el-table-column prop="jobDetail.concurrent" label="执行方式" width="100" align="center" show-overflow-tooltip>
 					<template #default="scope">
-						<el-tag v-if="scope.row.jobDetail.concurrent == true"> 并行 </el-tag>
-						<el-tag v-else> 串行 </el-tag>
+						<el-tag type="success" v-if="scope.row.jobDetail.concurrent == true"> 并行 </el-tag>
+						<el-tag type="warning" v-else> 串行 </el-tag>
 					</template>
 				</el-table-column>
-				<el-table-column prop="jobDetail.includeAnnotations" label="扫描特性触发器" align="center" show-overflow-tooltip>
+				<el-table-column prop="jobDetail.createFromScript" label="脚本创建" width="100" align="center" show-overflow-tooltip>
 					<template #default="scope">
-						<el-tag v-if="scope.row.includeAnnotations == true"> 是 </el-tag>
-						<el-tag v-else> 否 </el-tag>
+						<el-tag v-if="scope.row.jobDetail.createFromScript == true"> 是 </el-tag>
+						<el-tag type="info" v-else> 否 </el-tag>
 					</template>
 				</el-table-column>
+				<!-- <el-table-column prop="jobDetail.includeAnnotations" label="扫描特性触发器" align="center" show-overflow-tooltip>
+					<template #default="scope">
+						<el-tag v-if="scope.row.jobDetail.includeAnnotations == true"> 是 </el-tag>
+						<el-tag v-else> 否 </el-tag>
+					</template>
+				</el-table-column> -->
 				<el-table-column prop="jobDetail.properties" label="额外数据" show-overflow-tooltip />
 				<el-table-column prop="jobDetail.updatedTime" label="更新时间" width="160" align="center" show-overflow-tooltip />
 				<el-table-column label="操作" width="170" fixed="right" align="center" show-overflow-tooltip>
@@ -242,7 +248,7 @@ const resetQuery = () => {
 // 打开新增作业页面
 const openAddJobDetail = () => {
 	state.editJobDetailTitle = '添加作业';
-	editJobDetailRef.value?.openDialog({ concurrent: true, includeAnnotations: true, groupName: 'default' });
+	editJobDetailRef.value?.openDialog({ concurrent: true, includeAnnotations: true, groupName: 'default', createFromScript: true });
 };
 
 // 打开编辑作业页面
@@ -286,7 +292,7 @@ const delJobTrigger = (row: any) => {
 		type: 'warning',
 	})
 		.then(async () => {
-			await getAPI(SysJobApi).apiSysJobDeleteJobTriggerDelete({ triggerId: row.triggerId });
+			await getAPI(SysJobApi).apiSysJobDeleteJobTriggerDelete({ jobId: row.jobId, triggerId: row.triggerId });
 			handleQuery();
 			ElMessage.success('删除成功');
 		})