v3.4 在AntWeb应用提供web接口,供客户端走http接入

This commit is contained in:
大石头 2024-01-17 18:02:58 +08:00
parent cd95813f95
commit 8657a9d9e2
12 changed files with 375 additions and 94 deletions

View File

@ -7,7 +7,7 @@
<Description>调度中心下发C#或Sql给蚂蚁代理执行</Description>
<Company>新生命开发团队</Company>
<Copyright>版权所有(C) 新生命开发团队 2022</Copyright>
<VersionPrefix>3.3</VersionPrefix>
<VersionPrefix>3.4</VersionPrefix>
<VersionSuffix>$([System.DateTime]::Now.ToString(`yyyy.MMdd`))</VersionSuffix>
<Version>$(VersionPrefix).$(VersionSuffix)</Version>
<FileVersion>$(Version)</FileVersion>

View File

@ -4,8 +4,8 @@
<AssemblyTitle>蚂蚁数据</AssemblyTitle>
<Description>蚂蚁调度系统数据库结构</Description>
<Company>新生命开发团队</Company>
<Copyright>版权所有(C) 新生命开发团队 2020</Copyright>
<VersionPrefix>2.0</VersionPrefix>
<Copyright>版权所有(C) 新生命开发团队 2024</Copyright>
<VersionPrefix>3.4</VersionPrefix>
<VersionSuffix>$([System.DateTime]::Now.ToString(`yyyy.MMdd`))</VersionSuffix>
<Version>$(VersionPrefix).$(VersionSuffix)</Version>
<FileVersion>$(Version)</FileVersion>

View File

@ -5,8 +5,8 @@
<AssemblyTitle>蚂蚁数据调度SDK</AssemblyTitle>
<Description>分布式任务调度系统纯NET打造的重量级大数据实时计算平台万亿级调度经验积累。</Description>
<Company>新生命开发团队</Company>
<Copyright>©2002-2023 NewLife</Copyright>
<VersionPrefix>3.3</VersionPrefix>
<Copyright>©2002-2024 NewLife</Copyright>
<VersionPrefix>3.4</VersionPrefix>
<VersionSuffix>$([System.DateTime]::Now.ToString(`yyyy.MMdd`))</VersionSuffix>
<Version>$(VersionPrefix).$(VersionSuffix)</Version>
<FileVersion>$(Version)</FileVersion>

View File

@ -7,7 +7,7 @@
<Description>分布式任务调度系统纯NET打造的重量级大数据实时计算平台万亿级调度经验积累</Description>
<Company>新生命开发团队</Company>
<Copyright>版权所有(C) 新生命开发团队 2023</Copyright>
<VersionPrefix>3.3</VersionPrefix>
<VersionPrefix>3.4</VersionPrefix>
<VersionSuffix>$([System.DateTime]::Now.ToString(`yyyy.MMdd`))</VersionSuffix>
<Version>$(VersionPrefix).$(VersionSuffix)</Version>
<FileVersion>$(Version)</FileVersion>

View File

@ -58,7 +58,8 @@ class AntService : IApi, IActionFilter
{
_App = app;
var online = GetOnline(app, _Net);
var ip = _Net.Remote.Host;
var online = _appService.GetOnline(app, ip);
online.UpdateTime = TimerX.Now;
online.SaveAsync();
}
@ -81,7 +82,9 @@ class AntService : IApi, IActionFilter
else
XTrace.WriteException(ex);
WriteHistory(null, filterContext.ActionName, false, ex.GetMessage());
_Net = Session as INetSession;
var ip = _Net.Remote.Host;
_appService.WriteHistory(_App, filterContext.ActionName, false, ex.GetMessage(), ip);
}
}
}
@ -136,39 +139,8 @@ class AntService : IApi, IActionFilter
var job = model.Job?.Trim();
if (job.IsNullOrEmpty()) return new TaskModel[0];
return _jobService.Acquire(_App, model);
}
private void CheckErrorTask(App app, Job jb, Int32 count, List<JobTask> list)
{
// 每分钟检查一下错误任务和中断任务
var nextKey = $"_NextAcquireOld_{jb.ID}";
var now = TimerX.Now;
var ext = Session as IExtend;
var next = (DateTime)(ext[nextKey] ?? DateTime.MinValue);
if (next < now)
{
//var ps = ControllerContext.Current.Parameters;
//var server = ps["server"] + "";
//var pid = ps["pid"].ToInt();
var online = GetOnline(app, _Net);
var ip = _Net.Remote.Host;
next = now.AddSeconds(60);
list.AddRange(jb.AcquireOld(online.Server, ip, online.ProcessId, count, _cacheProvider.Cache));
if (list.Count > 0)
{
// 既然有数据,待会还来
next = now;
var n1 = list.Count(e => e.Status == JobStatus. || e.Status == JobStatus.);
var n2 = list.Count(e => e.Status == JobStatus. || e.Status == JobStatus. || e.Status == JobStatus.);
_log.Info("作业[{0}/{1}]准备处理[{2}]个错误和[{3}]超时任务 [{4}]", app, jb.Name, n1, n2, list.Join(",", e => e.ID + ""));
}
else
ext[nextKey] = next;
}
return _jobService.Acquire(_App, model, ip);
}
/// <summary>生产消息</summary>
@ -191,7 +163,8 @@ class AntService : IApi, IActionFilter
{
if (task == null || task.ID == 0) throw new InvalidOperationException("无效操作 TaskID=" + task?.ID);
return _jobService.Report(_App, task);
var ip = _Net.Remote.Host;
return _jobService.Report(_App, task, ip);
}
#endregion
}

View File

@ -9,6 +9,11 @@ using NewLife.Log;
using NewLife.Net;
using NewLife.Security;
using AntJob.Models;
using NewLife.Data;
using NewLife.Remoting;
using NewLife.Web;
using System.Reflection;
using System.Xml.Linq;
namespace AntJob.Server.Services;
@ -64,14 +69,11 @@ public class AppService
app.Save();
// 应用上线
var online = CreateOnline(app, _Net, model.Machine, model.ProcessId);
var online = CreateOnline(app, ip, model.Machine, model.ProcessId);
online.Version = model.Version;
online.CompileTime = model.Compile;
online.Save();
//// 记录当前用户
//Session["App"] = app;
WriteHistory(app, autoReg ? "注册" : "登录", true, $"[{model.User}/{model.Pass}]在[{model.Machine}@{model.ProcessId}]登录[{app}]成功");
var rs = new LoginResponse { Name = app.Name, DisplayName = app.DisplayName };
@ -133,49 +135,34 @@ public class AppService
return olts.Select(e => e.ToModel()).ToArray();
}
AppOnline CreateOnline(App app, INetSession ns, String machine, Int32 pid)
AppOnline CreateOnline(App app, String ip, String machine, Int32 pid)
{
var ip = ns.Remote.Host;
var online = GetOnline(app, ns);
var online = GetOnline(app, ip);
online.Client = $"{(ip.IsNullOrEmpty() ? machine : ip)}@{pid}";
online.Name = machine;
online.ProcessId = pid;
online.UpdateIP = ip;
//online.Version = version;
online.Server = Local + "";
online.Server = Environment.MachineName;
//online.Save();
// 真正的用户
Session["AppOnline"] = online;
// 下线
ns.OnDisposed += (s, e) =>
{
online.Delete();
WriteHistory(online.App, "下线", true, $"[{online.Name}]登录于{online.CreateTime},最后活跃于{online.UpdateTime}");
};
return online;
}
public AppOnline GetOnline(App app, INetSession ns)
public AppOnline GetOnline(App app, String ip)
{
if (Session["AppOnline"] is AppOnline online) return online;
var ip = ns.Remote.Host;
var ins = ns.Remote.EndPoint + "";
online = AppOnline.FindByInstance(ins) ?? new AppOnline { CreateIP = ip };
var ins = $"{app.Name}@{ip}";
var online = AppOnline.FindByInstance(ins) ?? new AppOnline { CreateIP = ip };
online.AppID = app.ID;
online.Instance = ins;
return online;
}
public void UpdateOnline(App app, JobTask ji, INetSession ns)
public void UpdateOnline(App app, JobTask ji, String ip)
{
var online = GetOnline(app, ns);
var online = GetOnline(app, ip);
online.Total += ji.Total;
online.Success += ji.Success;
online.Error += ji.Error;
@ -187,6 +174,77 @@ public class AppService
#endregion
#region
public void WriteHistory(App app, String action, Boolean success, String remark) => AppHistory.Create(app, action, success, remark, Local + "", _Net.Remote?.Host);
public void WriteHistory(App app, String action, Boolean success, String remark, String ip = null) =>
AppHistory.Create(app, action, success, remark, Environment.MachineName, ip);
#endregion
#region
public TokenModel IssueToken(String name, AntJobSetting set)
{
// 颁发令牌
var ss = set.TokenSecret.Split(':');
var jwt = new JwtBuilder
{
Issuer = Assembly.GetEntryAssembly().GetName().Name,
Subject = name,
Id = Rand.NextString(8),
Expire = DateTime.Now.AddSeconds(set.TokenExpire),
Algorithm = ss[0],
Secret = ss[1],
};
return new TokenModel
{
AccessToken = jwt.Encode(null),
TokenType = jwt.Type ?? "JWT",
ExpireIn = set.TokenExpire,
RefreshToken = jwt.Encode(null),
};
}
public (App, Exception) DecodeToken(String token, String tokenSecret)
{
if (token.IsNullOrEmpty()) throw new ArgumentNullException(nameof(token));
//if (token.IsNullOrEmpty()) throw new ApiException(401, $"节点未登录[ip={UserHost}]");
// 解码令牌
var ss = tokenSecret.Split(':');
var jwt = new JwtBuilder
{
Algorithm = ss[0],
Secret = ss[1],
};
var rs = jwt.TryDecode(token, out var message);
var app = App.FindByName(jwt.Subject);
Exception ex = null;
if (!rs || app == null)
{
if (app != null)
ex = new ApiException(403, $"[{app.Name}/{app.DisplayName}]非法访问 {message}");
else
ex = new ApiException(403, $"[{jwt.Subject}]非法访问 {message}");
}
return (app, ex);
}
public TokenModel ValidAndIssueToken(String deviceCode, String token, AntJobSetting set)
{
if (token.IsNullOrEmpty()) return null;
//var set = Setting.Current;
// 令牌有效期检查10分钟内过期者重新颁发令牌
var ss = set.TokenSecret.Split(':');
var jwt = new JwtBuilder
{
Algorithm = ss[0],
Secret = ss[1],
};
var rs = jwt.TryDecode(token, out var message);
return !rs || jwt == null ? null : DateTime.Now.AddMinutes(10) > jwt.Expire ? IssueToken(deviceCode, set) : null;
}
#endregion
}

View File

@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AntJob.Data;
using AntJob.Data;
using AntJob.Data.Entity;
using AntJob.Models;
using NewLife;
@ -87,7 +84,7 @@ public class JobService
/// <summary>申请作业任务</summary>
/// <param name="model">模型</param>
/// <returns></returns>
public ITask[] Acquire(App app, AcquireModel model)
public ITask[] Acquire(App app, AcquireModel model, String ip)
{
var job = model.Job?.Trim();
if (job.IsNullOrEmpty()) return new TaskModel[0];
@ -111,12 +108,12 @@ public class JobService
if (jb == null) throw new XException($"应用[{app.ID}/{app.Name}]下未找到作业[{job}]");
if (jb.Step == 0 || jb.Start.Year <= 2000) throw new XException("作业[{0}/{1}]未设置开始时间或步进", jb.ID, jb.Name);
var online = _appService.GetOnline(app, _Net);
var online = _appService.GetOnline(app, ip);
var list = new List<JobTask>();
// 每分钟检查一下错误任务和中断任务
CheckErrorTask(app, jb, model.Count, list);
CheckErrorTask(app, jb, model.Count, list, ip);
// 错误项不够时,增加切片
if (list.Count < model.Count)
@ -125,7 +122,6 @@ public class JobService
var server = online.Name;
var pid = online.ProcessId;
//var topic = ps["topic"] + "";
var ip = _Net.Remote.Host;
switch (jb.Mode)
{
@ -157,20 +153,15 @@ public class JobService
return list.Select(e => e.ToModel()).ToArray();
}
private void CheckErrorTask(App app, Job jb, Int32 count, List<JobTask> list)
private void CheckErrorTask(App app, Job jb, Int32 count, List<JobTask> list, String ip)
{
// 每分钟检查一下错误任务和中断任务
var nextKey = $"_NextAcquireOld_{jb.ID}";
var nextKey = $"antjob:NextAcquireOld_{jb.ID}";
var now = TimerX.Now;
var ext = Session as IExtend;
var next = (DateTime)(ext[nextKey] ?? DateTime.MinValue);
var next = _cacheProvider.Cache.Get<DateTime>(nextKey);
if (next < now)
{
//var ps = ControllerContext.Current.Parameters;
//var server = ps["server"] + "";
//var pid = ps["pid"].ToInt();
var online = _appService.GetOnline(app, _Net);
var ip = _Net.Remote.Host;
var online = _appService.GetOnline(app, ip);
next = now.AddSeconds(60);
list.AddRange(jb.AcquireOld(online.Server, ip, online.ProcessId, count, _cacheProvider.Cache));
@ -185,7 +176,7 @@ public class JobService
_log.Info("作业[{0}/{1}]准备处理[{2}]个错误和[{3}]超时任务 [{4}]", app, jb.Name, n1, n2, list.Join(",", e => e.ID + ""));
}
else
ext[nextKey] = next;
_cacheProvider.Cache.Set(nextKey, next);
}
}
@ -276,7 +267,7 @@ public class JobService
/// <summary>报告状态(进度、成功、错误)</summary>
/// <param name="task"></param>
/// <returns></returns>
public Boolean Report(App app, TaskResult task)
public Boolean Report(App app, TaskResult task, String ip)
{
if (task == null || task.ID == 0) throw new InvalidOperationException("无效操作 TaskID=" + task?.ID);
@ -308,7 +299,7 @@ public class JobService
SetJobFinish(job, jt);
// 记录状态
_appService.UpdateOnline(app, jt, _Net);
_appService.UpdateOnline(app, jt, ip);
}
if (task.Status == JobStatus.)
{

View File

@ -16,6 +16,18 @@ public class AntJobSetting : Config<AntJobSetting>
[Description("端口")]
public Int32 Port { get; set; } = 9999;
/// <summary>令牌密钥。用于生成JWT令牌的算法和密钥如HS256:ABCD1234</summary>
[Description("令牌密钥。用于生成JWT令牌的算法和密钥如HS256:ABCD1234")]
public String TokenSecret { get; set; }
/// <summary>令牌有效期。默认2*3600秒</summary>
[Description("令牌有效期。默认2*3600秒")]
public Int32 TokenExpire { get; set; } = 2 * 3600;
/// <summary>会话超时。默认600秒</summary>
[Description("会话超时。默认600秒")]
public Int32 SessionTimeout { get; set; } = 600;
/// <summary>自动注册。任意应用登录时自动注册省去人工配置应用账号的麻烦默认true</summary>
[Description("自动注册。任意应用登录时自动注册省去人工配置应用账号的麻烦默认true")]
public Boolean AutoRegistry { get; set; } = true;

View File

@ -6,7 +6,7 @@
<Description>分布式任务调度系统纯NET打造的重量级大数据实时计算平台万亿级调度经验积累</Description>
<Company>新生命开发团队</Company>
<Copyright>版权所有(C) 新生命开发团队 2023</Copyright>
<VersionPrefix>3.3</VersionPrefix>
<VersionPrefix>3.4</VersionPrefix>
<VersionSuffix>$([System.DateTime]::Now.ToString(`yyyy.MMdd`))</VersionSuffix>
<Version>$(VersionPrefix).$(VersionSuffix)</Version>
<FileVersion>$(Version)</FileVersion>
@ -52,7 +52,6 @@
</ItemGroup>
<ItemGroup>
<Folder Include="Areas\Ant\Views\AppOnline\" />
<Folder Include="Controllers\" />
<Folder Include="Services\" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,81 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using NewLife;
using NewLife.Log;
namespace AntJob.Web.Common;
/// <summary>统一Api过滤处理</summary>
/// <remarks>
/// 1解析访问令牌
/// 2包装响应结果为标准Json格式
/// 3拦截异常包装为标准Json格式
/// </remarks>
public sealed class ApiFilterAttribute : ActionFilterAttribute
{
/// <summary>从请求头中获取令牌</summary>
/// <param name="httpContext"></param>
/// <returns></returns>
public static String GetToken(HttpContext httpContext)
{
var request = httpContext.Request;
var token = request.Query["Token"] + "";
if (token.IsNullOrEmpty()) token = (request.Headers["Authorization"] + "").TrimStart("Bearer ");
if (token.IsNullOrEmpty()) token = request.Headers["X-Token"] + "";
if (token.IsNullOrEmpty()) token = request.Cookies["Token"] + "";
return token;
}
/// <summary>执行前,验证模型</summary>
/// <param name="context"></param>
public override void OnActionExecuting(ActionExecutingContext context)
{
//if (!context.ModelState.IsValid)
// throw new ApplicationException(context.ModelState.Values.First(p => p.Errors.Count > 0).Errors[0].ErrorMessage);
// 访问令牌
var token = GetToken(context.HttpContext);
context.HttpContext.Items["Token"] = token;
if (!context.ActionArguments.ContainsKey("token")) context.ActionArguments.Add("token", token);
base.OnActionExecuting(context);
}
/// <summary>执行后,包装结果和异常</summary>
/// <param name="context"></param>
public override void OnActionExecuted(ActionExecutedContext context)
{
if (context.HttpContext.WebSockets.IsWebSocketRequest) return;
if (context.Result != null)
if (context.Result is ObjectResult obj)
context.Result = new JsonResult(new { code = obj.StatusCode ?? 0, data = obj.Value });
else if (context.Result is EmptyResult)
{
DefaultTracer.Instance?.NewSpan("apiFilter-EmptyResult");
context.Result = new JsonResult(new { code = 0, data = new { } });
}
else if (context.Exception != null && !context.ExceptionHandled)
{
var ex = context.Exception.GetTrue();
if (ex is NewLife.Remoting.ApiException aex)
context.Result = new JsonResult(new { code = aex.Code, data = aex.Message });
else
{
context.Result = new JsonResult(new { code = 500, data = ex.Message });
// 埋点拦截业务异常
var action = context.HttpContext.Request.Path + "";
if (context.ActionDescriptor is ControllerActionDescriptor act) action = $"/{act.ControllerName}/{act.ActionName}";
DefaultTracer.Instance?.NewError(action, ex);
}
context.ExceptionHandled = true;
}
base.OnActionExecuted(context);
}
}

View File

@ -0,0 +1,167 @@
using System.Reflection;
using AntJob.Data;
using AntJob.Data.Entity;
using AntJob.Models;
using AntJob.Server;
using AntJob.Server.Services;
using AntJob.Web.Common;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Filters;
using NewLife;
using NewLife.Caching;
using NewLife.Cube;
using NewLife.Log;
using NewLife.Remoting;
using NewLife.Serialization;
using IActionFilter = Microsoft.AspNetCore.Mvc.Filters.IActionFilter;
namespace AntJob.Web.Controllers;
public class AntJobController : ControllerBase, IActionFilter
{
/// <summary>令牌</summary>
public String Token { get; private set; }
/// <summary>用户主机</summary>
public String UserHost => HttpContext.GetUserHost();
private App _App;
private IDictionary<String, Object> _args;
private AntJobSetting _setting;
private readonly AppService _appService;
private readonly JobService _jobService;
private readonly ICacheProvider _cacheProvider;
#region
public AntJobController(AppService appService, JobService jobService, AntJobSetting setting)
{
_appService = appService;
_jobService = jobService;
_setting = setting;
}
void IActionFilter.OnActionExecuting(ActionExecutingContext context)
{
_args = context.ActionArguments;
var token = Token = ApiFilterAttribute.GetToken(context.HttpContext);
try
{
if (context.ActionDescriptor is ControllerActionDescriptor act && !act.MethodInfo.IsDefined(typeof(AllowAnonymousAttribute)))
{
var rs = !token.IsNullOrEmpty() && OnAuthorize(token);
if (!rs) throw new ApiException(403, "认证失败");
}
}
catch (Exception ex)
{
var traceId = DefaultSpan.Current?.TraceId;
context.Result = ex is ApiException aex
? new JsonResult(new { code = aex.Code, data = aex.Message, traceId })
: new JsonResult(new { code = 500, data = ex.Message, traceId });
WriteError(ex, context);
}
}
void IActionFilter.OnActionExecuted(ActionExecutedContext context)
{
if (context.Exception != null) WriteError(context.Exception, context);
}
protected Boolean OnAuthorize(String token)
{
var (app, ex) = _appService.DecodeToken(token, _setting.TokenSecret);
_App = app;
if (ex != null) throw ex;
return app != null;
}
private void WriteError(Exception ex, ActionContext context)
{
// 拦截全局异常,写日志
var action = context.HttpContext.Request.Path + "";
if (context.ActionDescriptor is ControllerActionDescriptor act) action = $"{act.ControllerName}/{act.ActionName}";
_appService.WriteHistory(_App, action, false, ex?.GetTrue() + Environment.NewLine + _args?.ToJson(true), UserHost);
}
#endregion
#region
/// <summary>应用登录</summary>
/// <param name="model">模型</param>
/// <returns></returns>
[AllowAnonymous]
[HttpPost(nameof(Login))]
public LoginResponse Login(LoginModel model)
{
if (model.User.IsNullOrEmpty()) throw new ArgumentNullException(nameof(model.User));
var (app, rs) = _appService.Login(model, UserHost);
return rs;
}
/// <summary>获取当前应用的所有在线实例</summary>
/// <returns></returns>
[HttpGet(nameof(GetPeers))]
public PeerModel[] GetPeers() => _appService.GetPeers(_App);
#endregion
#region
/// <summary>获取指定名称的作业</summary>
/// <returns></returns>
[HttpGet(nameof(GetJobs))]
public IJob[] GetJobs() => _jobService.GetJobs(_App);
/// <summary>批量添加作业</summary>
/// <param name="jobs"></param>
/// <returns></returns>
[HttpPost(nameof(AddJobs))]
public String[] AddJobs(JobModel[] jobs)
{
if (jobs == null || jobs.Length == 0) return new String[0];
return _jobService.AddJobs(_App, jobs);
}
/// <summary>申请作业任务</summary>
/// <param name="model">模型</param>
/// <returns></returns>
[HttpPost(nameof(Acquire))]
public ITask[] Acquire(AcquireModel model)
{
var job = model.Job?.Trim();
if (job.IsNullOrEmpty()) return new TaskModel[0];
return _jobService.Acquire(_App, model, UserHost);
}
/// <summary>生产消息</summary>
/// <param name="model">模型</param>
/// <returns></returns>
[HttpPost(nameof(Produce))]
public Int32 Produce(ProduceModel model)
{
var messages = model?.Messages?.Where(e => !e.IsNullOrEmpty()).Distinct().ToArray();
if (messages == null || messages.Length == 0) return 0;
return _jobService.Produce(_App, model);
}
/// <summary>报告状态(进度、成功、错误)</summary>
/// <param name="task"></param>
/// <returns></returns>
[HttpPost(nameof(Report))]
public Boolean Report(TaskResult task)
{
if (task == null || task.ID == 0) throw new InvalidOperationException("无效操作 TaskID=" + task?.ID);
return _jobService.Report(_App, task, UserHost);
}
#endregion
}

View File

@ -5,8 +5,8 @@
<AssemblyTitle>蚂蚁调度SDK</AssemblyTitle>
<Description>分布式任务调度系统纯NET打造的重量级大数据实时计算平台万亿级调度经验积累。</Description>
<Company>新生命开发团队</Company>
<Copyright>©2002-2023 NewLife</Copyright>
<VersionPrefix>3.3</VersionPrefix>
<Copyright>©2002-2024 NewLife</Copyright>
<VersionPrefix>3.4</VersionPrefix>
<VersionSuffix>$([System.DateTime]::Now.ToString(`yyyy.MMdd`))</VersionSuffix>
<Version>$(VersionPrefix).$(VersionSuffix)</Version>
<FileVersion>$(Version)</FileVersion>