Compare commits

...

49 Commits

Author SHA1 Message Date
Diego
79789388fc fix: opcuaserver动态刷新变量节点会导致新建的动态节点无法刷新订阅 2025-07-02 19:11:11 +08:00
Diego
2c4194ee18 refactor: opcua某些不存在的节点ID不再影响整体订阅,只出现日志提示 2025-07-02 15:09:52 +08:00
Diego
1b2be585af fix: 报警分析错误设置循环 2025-07-02 14:26:25 +08:00
Diego
83736647e7 feat: S7PLC增加WString支持 2025-07-02 12:49:55 +08:00
Diego
b06405717d build: 10.9.9
fix: timerx 池 max值取消
feat: mqttrpc支持脚本
2025-07-02 10:03:50 +08:00
2248356998 qq.com
298a1f2ed4 更新docker文件 2025-07-02 07:32:23 +08:00
Diego
74a47a1983 build: 10.9.8
支持redis缓存
2025-07-01 17:55:03 +08:00
Diego
a48a42abe4 modbusslave 异常捕获 2025-07-01 10:51:10 +08:00
Diego
feb1d0a3c5 feat: 增加后台服务生命周期识别 2025-06-30 10:59:18 +08:00
Diego
92bca824e6 10.9.6 2025-06-29 22:19:53 +08:00
2248356998 qq.com
025c699517 feat: s7增加请求id识别 2025-06-29 22:06:53 +08:00
2248356998 qq.com
53f8fbe4b1 refactor: 变量排序导出 2025-06-28 21:43:06 +08:00
2248356998 qq.com
77bfabc41d feat: 改用Mapperly源生成,代替Mapster 2025-06-28 00:00:43 +08:00
Diego
6427ee6ee0 refactor: 降低sqlite依赖 2025-06-27 14:44:00 +08:00
Diego
4c95997d62 build: 10.9.2
fix: taos connection dispose
refactor: opcua AddSubscriptionAsync 添加延时和重试
2025-06-27 11:16:58 +08:00
Diego
1d82cea40d build: 10.9.1
fix: 任务空错误异常
feat: 数据库插件增加字符串变量表和数值变量表两种情况
2025-06-27 03:02:03 +08:00
Diego
e78799424c 添加日志输出筛选 2025-06-25 17:09:05 +08:00
Diego
1000c8d38f docs: 更新采集插件开发文档 2025-06-25 14:22:58 +08:00
Diego
b2589fc634 build: 10.8.24
fix: 变量离线后再次上线,如果值不变,会导致在线状态不刷新
fix: s7离线恢复时,可能触发多次协议握手导致异常
2025-06-25 11:19:06 +08:00
Diego
c80e57a4e8 build: 10.8.24
fix: 变量离线后再次上线,如果值不变,会导致在线状态不刷新
fix: s7离线恢复时,可能触发多次协议握手导致异常
2025-06-25 11:17:04 +08:00
Diego
6510c3e289 feat: 增加一个变量读写表达式常用转换的友好编辑界面 2025-06-24 16:14:35 +08:00
Diego
920e407d05 恢复规则引擎脚本接口 2025-06-24 10:52:00 +08:00
Diego
7314c8901d fix: 用户编辑框初始刷新职位 2025-06-24 09:15:33 +08:00
Diego
4e9f02b48c 更新依赖包 2025-06-24 09:03:34 +08:00
2248356998 qq.com
9ae7602cb4 配置最大连接数 2025-06-24 00:09:07 +08:00
2248356998 qq.com
aa8aa36aef build: 10.8.19
fix: s7 复用地址对象导致读取异常
feat: 规则引擎node添加内部异常捕获
feat: 变量增加属性: 写入后再次读取检查值是否一致
2025-06-23 21:21:27 +08:00
2248356998 qq.com
0174f7c6f2 更新依赖 2025-06-22 23:05:12 +08:00
2248356998 qq.com
df9e7d6ff1 10.8.17 2025-06-22 21:11:37 +08:00
Diego
b40ca920d3 fix: 变量自动刷新运行态 2025-06-20 16:58:45 +08:00
Diego
5a4b0a0e93 修改插件过期提示 2025-06-20 14:43:13 +08:00
Diego
a879edd68b 10.8.12 2025-06-20 13:52:42 +08:00
Diego
62e0a6ee9d gitee登录按钮隐藏 2025-06-19 17:04:22 +08:00
Diego
765e5564d4 10.8.10 2025-06-19 16:56:04 +08:00
Diego
10eecac19b feat: 优化设备状态逻辑 2025-06-19 11:41:43 +08:00
Diego
59241b8faa fix: opcua插件订阅检查失效 2025-06-19 10:42:54 +08:00
Diego
52b3097f04 10.8.7 2025-06-18 22:04:48 +08:00
Diego
d922296b70 feat: 重构插件任务 2025-06-18 17:11:57 +08:00
Diego
aec91da28b 10.8.2
feat: 优化闭包导致的状态机内存占用,高并发时内存显著下降
fix(sqldb): 定时上传模式时,实时表时效
fix(taos): 初始化失败
2025-06-17 17:09:05 +08:00
Diego
013ff394be 10.8.1 2025-06-16 18:25:55 +08:00
Diego
081e07473d 10.8.0 2025-06-16 18:12:28 +08:00
Diego
d33d900592 增加github oauth登录 2025-06-15 00:17:25 +08:00
Diego
29365c4ef9 修改脚本测试方法 2025-06-14 11:56:31 +08:00
Diego
17a6189089 style: 更新SysResourcePage页面样式 2025-06-13 16:03:28 +08:00
Diego
003b8a3763 10.7.56 2025-06-13 13:35:38 +08:00
Diego
1c7f8b5cab 10.7.55 2025-06-13 09:14:22 +08:00
Diego
b7ff9ffca2 更新解决方案 2025-06-13 09:06:42 +08:00
Diego
9bba9bda76 10.7.54 2025-06-12 23:54:10 +08:00
Diego
ec2fcc75d3 合并代码 2025-06-12 20:21:50 +08:00
Diego
57a4038577 pwa安装提示优化 2025-06-12 10:20:52 +08:00
1183 changed files with 59586 additions and 11107 deletions

View File

@@ -85,7 +85,7 @@
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
as of the date such litigation is field.
4. Cachetribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without

View File

@@ -0,0 +1,6 @@
## Release 1.0
### New Rules
Rule ID | Category | Severity | Notes
--------|----------|----------|--------------------
TG0001 | Conflict | Error | SetParametersAsyncGenerator

View File

@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\..\PackNuget.props" />
<PropertyGroup>
<TargetFrameworks>netstandard2.0;</TargetFrameworks>
<Version>$(SourceGeneratorVersion)</Version>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<NoPackageAnalysis>true</NoPackageAnalysis>
<SignAssembly>false</SignAssembly>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<IncludeBuildOutput>false</IncludeBuildOutput>
<!-- 避免 DLL 被打包到 lib/ -->
<EnableSourceGenerator>true</EnableSourceGenerator>
<!-- 可选 -->
</PropertyGroup>
<ItemGroup>
<None Include="$(OutputPath)\$(TargetFileName)" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="AnalyzerReleases.Shipped.md" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.13.0" PrivateAssets="all" Private="false" />
</ItemGroup>
</Project>

View File

@@ -7,12 +7,3 @@
// 使用文档https://thingsgateway.cn/
// QQ群605534569
//------------------------------------------------------------------------------
global using BootstrapBlazor.Components;
global using Microsoft.AspNetCore.Components;
global using Microsoft.Extensions.Localization;
global using System.Diagnostics.CodeAnalysis;
global using ThingsGateway.Razor;

View File

@@ -0,0 +1,439 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System.Text;
namespace Microsoft.AspNetCore.Components;
[Generator]
public sealed partial class SetParametersAsyncGenerator : IIncrementalGenerator
{
private const string m_DoNotGenerateSetParametersAsyncAttribute = """
using System;
namespace Microsoft.AspNetCore.Components
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
internal sealed class DoNotGenerateSetParametersAsyncAttribute : Attribute { }
}
""";
private const string m_GenerateSetParametersAsyncAttribute = """
using System;
namespace Microsoft.AspNetCore.Components
{
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
internal sealed class GenerateSetParametersAsyncAttribute : Attribute
{
public bool RequireExactMatch { get; set; }
}
}
""";
private const string m_GlobalGenerateSetParametersAsyncAttribute = """
using System;
namespace Microsoft.AspNetCore.Components
{
[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)]
internal sealed class GlobalGenerateSetParametersAsyncAttribute : Attribute
{
public bool Enable { get; }
public GlobalGenerateSetParametersAsyncAttribute(bool enable = true) { Enable = enable; }
}
}
""";
private static readonly DiagnosticDescriptor ParameterNameConflict = new DiagnosticDescriptor(
id: "TG0001",
title: "Parameter name conflict",
messageFormat: "Parameter names are case insensitive. {0} conflicts with {1}.",
category: "Conflict",
defaultSeverity: DiagnosticSeverity.Error,
isEnabledByDefault: true,
description: "Parameter names must be case insensitive to be usable in routes. Rename the parameter to not be in conflict with other parameters.");
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// 注入 attribute 源码
context.RegisterPostInitializationOutput(ctx =>
{
ctx.AddSource("DoNotGenerateSetParametersAsyncAttribute.g.cs", SourceText.From(m_DoNotGenerateSetParametersAsyncAttribute, Encoding.UTF8));
ctx.AddSource("GenerateSetParametersAsyncAttribute.g.cs", SourceText.From(m_GenerateSetParametersAsyncAttribute, Encoding.UTF8));
ctx.AddSource("GlobalGenerateSetParametersAsyncAttribute.g.cs", SourceText.From(m_GlobalGenerateSetParametersAsyncAttribute, Encoding.UTF8));
});
// 筛选 ClassDeclarationSyntax
var classDeclarations = context.SyntaxProvider
.CreateSyntaxProvider(
predicate: static (node, _) => node is ClassDeclarationSyntax,
transform: static (ctx, _) => (ClassDeclarationSyntax)ctx.Node)
.Where(static c => c is not null);
// 合并 Compilation
var compilationProvider = context.CompilationProvider;
var candidateClasses = classDeclarations.Combine(compilationProvider);
context.RegisterSourceOutput(candidateClasses, static (spc, tuple) =>
{
var (classDeclaration, compilation) = tuple;
Execute(spc, compilation, classDeclaration);
});
}
private static void Execute(SourceProductionContext context, Compilation compilation, ClassDeclarationSyntax classDeclaration)
{
var model = compilation.GetSemanticModel(classDeclaration.SyntaxTree);
var classSymbol = model.GetDeclaredSymbol(classDeclaration);
if (classSymbol is null || classSymbol.Name == "_Imports")
return;
var positiveAttr = compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Components.GenerateSetParametersAsyncAttribute");
var negativeAttr = compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Components.DoNotGenerateSetParametersAsyncAttribute");
if (classSymbol.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, negativeAttr)))
return;
if (!IsPartial(classSymbol) || !IsComponent(classDeclaration, classSymbol, compilation))
return;
var globalEnable = compilation.Assembly.GetAttributes()
.FirstOrDefault(a => a.AttributeClass?.ToDisplayString() == "Microsoft.AspNetCore.Components.GlobalGenerateSetParametersAsyncAttribute")
?.ConstructorArguments.FirstOrDefault().Value as bool? ?? false;
var hasPositive = classSymbol.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, positiveAttr));
if (!globalEnable && !hasPositive)
return;
GenerateSetParametersAsyncMethod(context, classSymbol);
}
private static void GenerateSetParametersAsyncMethod(SourceProductionContext context, INamedTypeSymbol class_symbol)
{
var force_exact_match = class_symbol.GetAttributes().Any(a => a.NamedArguments.Any(na => na.Key == "RequireExactMatch" && na.Value.Value is bool v && v));
var namespaceName = class_symbol.ContainingNamespace.ToDisplayString();
var type_kind = class_symbol.TypeKind switch { TypeKind.Class => "class", TypeKind.Interface => "interface", _ => "struct" };
var type_parameters = string.Join(", ", class_symbol.TypeArguments.Select(t => t.Name));
type_parameters = string.IsNullOrEmpty(type_parameters) ? type_parameters : "<" + type_parameters + ">";
context.AddCode(class_symbol.ToDisplayString() + "_override.cs", $@"
using Microsoft.AspNetCore.Components;
using System.Collections.Generic;
using System.Threading.Tasks;
#pragma warning disable CA2007
#pragma warning disable CS0162
#pragma warning disable CS8632
namespace {namespaceName}
{{
public partial class {class_symbol.Name}{type_parameters}
{{
private bool _initialized;
/// <summary>
/// <inheritdoc/>
/// </summary>
public override Task SetParametersAsync(ParameterView parameters)
{{
Dictionary<string,object?> parameterValues = new();
foreach (var parameter in parameters)
{{
if(BlazorImplementation__WriteSingleParameter(parameter.Name, parameter.Value)==false)
{{
// 如果没有处理参数,则添加到参数列表中
parameterValues.Add(parameter.Name, parameter.Value);
}}
}}
if(parameterValues.Count > 0)
{{
parameters.SetParameterProperties(this);
}}
if (!_initialized)
{{
_initialized = true;
return RunInitAndSetParametersAsync();
}}
else
{{
return CallOnParametersSetAsync();
}}
}}
// We do not want the debugger to consider NavigationExceptions caught by this method as user-unhandled.
#if NET9_0_OR_GREATER
[System.Diagnostics.DebuggerDisableUserUnhandledExceptions]
#endif
private async Task RunInitAndSetParametersAsync()
{{
Task task;
try
{{
OnInitialized();
task = OnInitializedAsync();
}}
catch (Exception ex) when (ex is not NavigationException)
{{
throw;
}}
if (task.Status != TaskStatus.RanToCompletion && task.Status != TaskStatus.Canceled)
{{
// Call state has changed here so that we render after the sync part of OnInitAsync has run
// and wait for it to finish before we continue. If no async work has been done yet, we want
// to defer calling StateHasChanged up until the first bit of async code happens or until
// the end. Additionally, we want to avoid calling StateHasChanged if no
// async work is to be performed.
StateHasChanged();
try
{{
await task;
}}
catch // avoiding exception filters for AOT runtime support
{{
// Ignore exceptions from task cancellations.
// Awaiting a canceled task may produce either an OperationCanceledException (if produced as a consequence of
// CancellationToken.ThrowIfCancellationRequested()) or a TaskCanceledException (produced as a consequence of awaiting Task.FromCanceled).
// It's much easier to check the state of the Task (i.e. Task.IsCanceled) rather than catch two distinct exceptions.
if (!task.IsCanceled)
{{
throw;
}}
}}
// Don't call StateHasChanged here. CallOnParametersSetAsync should handle that for us.
}}
await CallOnParametersSetAsync();
}}
// We do not want the debugger to consider NavigationExceptions caught by this method as user-unhandled.
#if NET9_0_OR_GREATER
[System.Diagnostics.DebuggerDisableUserUnhandledExceptions]
#endif
private Task CallOnParametersSetAsync()
{{
Task task;
try
{{
OnParametersSet();
task = OnParametersSetAsync();
}}
catch (Exception ex) when (ex is not NavigationException)
{{
#if NET9_0_OR_GREATER
System.Diagnostics.Debugger.BreakForUserUnhandledException(ex);
#endif
throw;
}}
// If no async work is to be performed, i.e. the task has already ran to completion
// or was canceled by the time we got to inspect it, avoid going async and re-invoking
// StateHasChanged at the culmination of the async work.
var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion &&
task.Status != TaskStatus.Canceled;
// We always call StateHasChanged here as we want to trigger a rerender after OnParametersSet and
// the synchronous part of OnParametersSetAsync has run.
StateHasChanged();
return shouldAwaitTask ?
CallStateHasChangedOnAsyncCompletion(task) :
Task.CompletedTask;
}}
// We do not want the debugger to stop more than once per user-unhandled exception.
#if NET9_0_OR_GREATER
[System.Diagnostics.DebuggerDisableUserUnhandledExceptions]
#endif
private async Task CallStateHasChangedOnAsyncCompletion(Task task)
{{
try
{{
await task;
}}
catch // avoiding exception filters for AOT runtime support
{{
// Ignore exceptions from task cancellations, but don't bother issuing a state change.
if (task.IsCanceled)
{{
return;
}}
throw;
}}
StateHasChanged();
}}
}}
}}
#pragma warning restore CS8632
#pragma warning restore CS0162
#pragma warning restore CA2007
");
var bases = class_symbol.GetTypeHierarchy().Where(t => !SymbolEqualityComparer.Default.Equals(t, class_symbol));
var members = class_symbol.GetMembers() // members of the type itself
.Concat(bases.SelectMany(t => t.GetMembers().Where(m => m.DeclaredAccessibility != Accessibility.Private))) // plus accessible members of any base
.Distinct(SymbolEqualityComparer.Default);
var property_symbols = members.OfType<IPropertySymbol>();
var writable_property_symbols = property_symbols.Where(ps =>
!ps.IsReadOnly || ps.GetAttributes().Any(a =>
a.AttributeClass?.Name is "CascadingParameter" or "CascadingParameterAttribute")
);
var parameter_symbols = writable_property_symbols
.Where(ps => ps.GetAttributes().Any(a => false
|| a.AttributeClass.Name == "Parameter"
|| a.AttributeClass.Name == "ParameterAttribute"
|| a.AttributeClass.Name == "CascadingParameter"
|| a.AttributeClass.Name == "CascadingParameterAttribute"
));
var name_conflicts = parameter_symbols.GroupBy(ps => ps.Name.ToLowerInvariant()).Where(g => g.Count() > 1);
foreach (var conflict in name_conflicts)
{
var key = conflict.Key;
var conflicting_parameters = conflict.ToList();
foreach (var parameter in conflicting_parameters)
{
var this_name = parameter.Name;
var conflicting_name = conflicting_parameters.Select(p => p.Name).FirstOrDefault(n => n != this_name);
foreach (var location in parameter.Locations)
{
context.ReportDiagnostic(Diagnostic.Create(ParameterNameConflict, location, this_name, conflicting_name));
}
}
}
var all = parameter_symbols.ToList();
var catch_all_parameter = parameter_symbols.FirstOrDefault(p =>
{
var parameter_attr = p.GetAttributes().FirstOrDefault(a => a.AttributeClass!.Name.StartsWith("Parameter"));
return parameter_attr?.NamedArguments.Any(n => n.Key == "CaptureUnmatchedValues" && n.Value.Value is bool v && v) == true;
});
var lower_case_match_cases = parameter_symbols.Except(new[] { catch_all_parameter }).Select(p => $"case \"{p.Name.ToLowerInvariant()}\": this.{p.Name} = ({p.Type.ToDisplayString()}) value; break;");
var lower_case_match_default = catch_all_parameter == null ? @"default: {return false;}" : $@"
default:
{{
this.{catch_all_parameter.Name} ??= new System.Collections.Generic.Dictionary<string, object>();
var writable_dict = this.{catch_all_parameter.Name};
if (!writable_dict.TryAdd(name, value))
{{
writable_dict[name] = value;
}}
break;
}}";
var exact_match_cases = parameter_symbols.Except(new[] { catch_all_parameter }).Select(p => $"case \"{p!.Name}\": this.{p.Name} = ({p.Type.ToDisplayString()}) value; break;");
string exact_match_default;
if (force_exact_match)
{
if (catch_all_parameter == null) // exact matches are forced, and we do not have a catch-all parameter, therefore we need to throw on unmatched parameter
{
exact_match_default = @"default: { return false;";
}
else // exact matches are forced, and we DO have a catch-all parameter, therefore we simply add that unmatched parameter to the dictionary
{
exact_match_default = $@"
default:
{{
this.{catch_all_parameter.Name} ??= new System.Collections.Generic.Dictionary<string, object>();
var writable_dict = this.{catch_all_parameter.Name};
if (!writable_dict.TryAdd(name, value))
{{
writable_dict[name] = value;
}}
break;
}}";
}
}
else
{
// exact matches are not forced, so if there is no exact match, we fall back to compare it in lower case
exact_match_default = $@"
default:
{{
switch (name.ToLowerInvariant())
{{
{string.Join("\n", lower_case_match_cases)}
{lower_case_match_default}
}}
break;
}}
";
}
context.AddCode(class_symbol.ToDisplayString() + "_implementation.cs", $@"
using System;
#pragma warning disable CS0162
#pragma warning disable CS0618
#pragma warning disable CS8632
namespace {namespaceName}
{{
public partial class {class_symbol.Name}{type_parameters}
{{
private bool BlazorImplementation__WriteSingleParameter(string name, object value)
{{
if(name != ""Body"")
{{
switch (name)
{{
{string.Join("\n", exact_match_cases)}
{exact_match_default}
}}
return true;
}}
return false;
}}
}}
}}
#pragma warning restore CS8632
#pragma warning restore CS0618
#pragma warning restore CS0162");
}
private static bool IsPartial(INamedTypeSymbol symbol)
{
return symbol.DeclaringSyntaxReferences
.Select(r => r.GetSyntax())
.OfType<ClassDeclarationSyntax>()
.Any(c => c.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)));
}
private static bool IsComponent(ClassDeclarationSyntax classDeclaration, INamedTypeSymbol symbol, Compilation compilation)
{
if (HasUserDefinedSetParametersAsync(symbol))
return false;
if (classDeclaration.SyntaxTree.FilePath.EndsWith(".razor") || classDeclaration.SyntaxTree.FilePath.EndsWith(".razor.cs"))
return true;
var iComponent = compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Components.IComponent");
var componentBase = compilation.GetTypeByMetadataName("Microsoft.AspNetCore.Components.ComponentBase");
if (iComponent == null || componentBase == null)
return false;
return symbol.AllInterfaces.Contains(iComponent) || SymbolEqualityComparer.Default.Equals(symbol.BaseType, componentBase);
}
private static bool HasUserDefinedSetParametersAsync(INamedTypeSymbol classSymbol)
{
return classSymbol
.GetMembers("SetParametersAsync")
.OfType<IMethodSymbol>()
.Any(m =>
m.Parameters.Length == 1 &&
m.Parameters[0].Type.ToDisplayString() == "Microsoft.AspNetCore.Components.ParameterView" &&
m.DeclaredAccessibility == Accessibility.Public &&
!m.IsStatic);
}
}

View File

@@ -0,0 +1,15 @@
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Text;
namespace Microsoft.AspNetCore.Components
{
internal static class SourceGeneratorContextExtension
{
public static void AddCode(this SourceProductionContext context, string hint_name, string code)
{
context.AddSource(hint_name.Replace("<", "_").Replace(">", "_"), SourceText.From(code, Encoding.UTF8));
}
}
}

View File

@@ -0,0 +1,19 @@
using Microsoft.CodeAnalysis;
namespace Microsoft.AspNetCore.Components
{
public static class TypeSymbolExtension
{
public static IEnumerable<INamedTypeSymbol> GetTypeHierarchy(this INamedTypeSymbol symbol)
{
yield return symbol;
if (symbol.BaseType != null)
{
foreach (var type in GetTypeHierarchy(symbol.BaseType))
{
yield return type;
}
}
}
}
}

View File

@@ -0,0 +1,49 @@
param($installPath, $toolsPath, $package, $project)
$analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers" ) * -Resolve
foreach($analyzersPath in $analyzersPaths)
{
# Install the language agnostic analyzers.
if (Test-Path $analyzersPath)
{
foreach ($analyzerFilePath in Get-ChildItem $analyzersPath -Filter *.dll)
{
if($project.Object.AnalyzerReferences)
{
$project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName)
}
}
}
}
# $project.Type gives the language name like (C# or VB.NET)
$languageFolder = ""
if($project.Type -eq "C#")
{
$languageFolder = "cs"
}
if($project.Type -eq "VB.NET")
{
$languageFolder = "vb"
}
if($languageFolder -eq "")
{
return
}
foreach($analyzersPath in $analyzersPaths)
{
# Install language specific analyzers.
$languageAnalyzersPath = join-path $analyzersPath $languageFolder
if (Test-Path $languageAnalyzersPath)
{
foreach ($analyzerFilePath in Get-ChildItem $languageAnalyzersPath -Filter *.dll)
{
if($project.Object.AnalyzerReferences)
{
$project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName)
}
}
}
}

View File

@@ -0,0 +1,56 @@
param($installPath, $toolsPath, $package, $project)
$analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers" ) * -Resolve
foreach($analyzersPath in $analyzersPaths)
{
# Uninstall the language agnostic analyzers.
if (Test-Path $analyzersPath)
{
foreach ($analyzerFilePath in Get-ChildItem $analyzersPath -Filter *.dll)
{
if($project.Object.AnalyzerReferences)
{
$project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName)
}
}
}
}
# $project.Type gives the language name like (C# or VB.NET)
$languageFolder = ""
if($project.Type -eq "C#")
{
$languageFolder = "cs"
}
if($project.Type -eq "VB.NET")
{
$languageFolder = "vb"
}
if($languageFolder -eq "")
{
return
}
foreach($analyzersPath in $analyzersPaths)
{
# Uninstall language specific analyzers.
$languageAnalyzersPath = join-path $analyzersPath $languageFolder
if (Test-Path $languageAnalyzersPath)
{
foreach ($analyzerFilePath in Get-ChildItem $languageAnalyzersPath -Filter *.dll)
{
if($project.Object.AnalyzerReferences)
{
try
{
$project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName)
}
catch
{
}
}
}
}
}

View File

@@ -20,6 +20,7 @@ using System.Collections.Concurrent;
using ThingsGateway.Extension;
using ThingsGateway.FriendlyException;
using ThingsGateway.NewLife.Json.Extension;
using ThingsGateway.SqlSugar;
namespace ThingsGateway.Admin.Application;
@@ -90,13 +91,12 @@ public sealed class OperDescAttribute : MoAttribute
OperDescAttribute.WriteToQueue(log);
}
}
private static SqlSugarClient _db = DbContext.GetDB<SysOperateLog>();
/// <summary>
/// 将日志消息写入数据库中
/// </summary>
private static async Task ProcessQueue()
{
var db = DbContext.Db.GetConnectionScopeWithAttr<SysOperateLog>().CopyNew();
var appLifetime = App.RootServices!.GetService<IHostApplicationLifetime>()!;
while (!appLifetime.ApplicationStopping.IsCancellationRequested)
{
@@ -105,7 +105,7 @@ public sealed class OperDescAttribute : MoAttribute
var data = _logMessageQueue.ToListWithDequeue(); // 从日志队列中获取数据
if (data.Count > 0)
{
await db.InsertableWithAttr(data).ExecuteCommandAsync(appLifetime.ApplicationStopping).ConfigureAwait(false);//入库
await _db.InsertableWithAttr(data).ExecuteCommandAsync(appLifetime.ApplicationStopping).ConfigureAwait(false);//入库
}
}
catch (Exception ex)

View File

@@ -33,22 +33,22 @@ public static class CacheConst
/// <summary>
/// 资源表缓存Key
/// </summary>
public const string Cache_SysResource = $"{CacheConst.Cache_Prefix_Admin}SysResource:";
public const string Cache_SysResource = $"{CacheConst.Cache_Prefix_Admin}SysResource:List";
/// <summary>
/// 角色表缓存Key
/// </summary>
public const string Cache_SysRole = $"{CacheConst.Cache_Prefix_Admin}SysRole:";
public const string Cache_SysRole = $"{CacheConst.Cache_Prefix_Admin}SysRole:List";
/// <summary>
/// 用户表缓存Key
/// </summary>
public const string Cache_SysUser = $"{CacheConst.Cache_Prefix_Admin}SysUser:";
public const string Cache_SysUser = $"{CacheConst.Cache_Prefix_Admin}SysUser:Hash";
/// <summary>
/// 用户账号关系缓存Key
/// </summary>
public const string Cache_SysUserAccount = $"{CacheConst.Cache_Prefix_Admin}SysUserAccount:";
public const string Cache_SysUserAccount = $"{CacheConst.Cache_Prefix_Admin}SysUserAccount:Hash";
/// <summary>
/// 职位表缓存Key
@@ -58,7 +58,7 @@ public static class CacheConst
/// <summary>
/// 机构表缓存Key
/// </summary>
public const string Cache_SysOrg = $"{CacheConst.Cache_Prefix_Admin}SysOrg:";
public const string Cache_SysOrg = $"{CacheConst.Cache_Prefix_Admin}SysOrg:List";
/// <summary>
/// 公司表缓存Key
@@ -67,12 +67,12 @@ public static class CacheConst
/// <summary>
/// 公司表缓存Key
/// </summary>
public const string Cache_SysOrgTenant = $"{CacheConst.Cache_Prefix_Admin}OrgTenant:";
public const string Cache_SysOrgTenant = $"{CacheConst.Cache_Prefix_Admin}OrgTenant:Hash";
/// <summary>
/// Token表缓存Key
/// </summary>
public const string Cache_Token = $"{CacheConst.Cache_Prefix_Admin}Token:";
public const string Cache_Token = $"{CacheConst.Cache_Prefix_Admin}Token:Hash";
#region

View File

@@ -8,8 +8,6 @@
// QQ群605534569
//------------------------------------------------------------------------------
using Mapster;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@@ -41,9 +39,9 @@ public class OpenApiController : ControllerBase
[AllowAnonymous]
public async Task<OpenApiLoginOutput> LoginAsync([FromBody] OpenApiLoginInput input)
{
var output = await _authService.LoginAsync(input.Adapt<LoginInput>(), false).ConfigureAwait(false);
var output = await _authService.LoginAsync(input.AdaptLoginInput(), false).ConfigureAwait(false);
var openApiLoginOutput = output.Adapt<OpenApiLoginOutput>();
var openApiLoginOutput = output.AdaptOpenApiLoginOutput();
return openApiLoginOutput;
}

View File

@@ -10,7 +10,7 @@
using BootstrapBlazor.Components;
using Mapster;
using Riok.Mapperly.Abstractions;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
@@ -31,7 +31,7 @@ public class SysUser : BaseEntity
///</summary>
[SugarColumn(ColumnDescription = "头像", ColumnDataType = StaticConfig.CodeFirst_BigString, IsNullable = true)]
[AutoGenerateColumn(Visible = true, Sortable = false, Filterable = false)]
[AdaptIgnore]
[MapperIgnore]
public virtual string? Avatar { get; set; }
/// <summary>

View File

@@ -267,7 +267,7 @@ public class RequestAuditFilter : IAsyncActionFilter, IOrderedFilter
}
else
{
logger.Log(LogLevel.Warning, $"{logData.Method}:{logData.Path}-{logData.Operation}{Environment.NewLine}{logData.Exception.ToSystemTextJsonString()}");
logger.Log(LogLevel.Warning, $"{logData.Method}:{logData.Path}-{logData.Operation}{Environment.NewLine}{logData.Exception?.ToSystemTextJsonString()}{Environment.NewLine}{logData.Validation?.ToSystemTextJsonString()}");
}
}

View File

@@ -1,40 +0,0 @@
using Microsoft.AspNetCore.Authentication.OAuth;
using System.Text.Json;
namespace ThingsGateway.Admin.Application;
/// <summary>OAuthOptions 配置类</summary>
public abstract class AdminOAuthOptions : OAuthOptions
{
/// <summary>默认构造函数</summary>
protected AdminOAuthOptions()
{
ConfigureClaims();
this.Events.OnRemoteFailure = context =>
{
var redirectUri = string.IsNullOrEmpty(HomePath) ? "/" : HomePath;
context.Response.Redirect(redirectUri);
context.HandleResponse();
return Task.CompletedTask;
};
}
/// <summary>配置 Claims 映射</summary>
protected virtual void ConfigureClaims()
{
}
public virtual string GetName(JsonElement element)
{
JsonElement.ObjectEnumerator target = element.EnumerateObject();
return target.TryGetValue("name");
}
/// <summary>获得/设置 登陆后首页</summary>
public string HomePath { get; set; } = "/";
}

View File

@@ -1,12 +0,0 @@
namespace ThingsGateway.Admin.Application;
public class GiteeOAuthUser
{
public string Id { get; set; }
public string Login { get; set; }
public string Name { get; set; }
public string Avatar_Url { get; set; }
}

View File

@@ -1,22 +0,0 @@
using System.Text.Json;
namespace ThingsGateway.Admin.Application;
public static class OAuthUserExtensions
{
public static GiteeOAuthUser ToAuthUser(this JsonElement element)
{
GiteeOAuthUser authUser = new GiteeOAuthUser();
JsonElement.ObjectEnumerator target = element.EnumerateObject();
authUser.Id = target.TryGetValue("id");
authUser.Login = target.TryGetValue("login");
authUser.Name = target.TryGetValue("name");
authUser.Avatar_Url = target.TryGetValue("avatar_url");
return authUser;
}
public static string TryGetValue(this JsonElement.ObjectEnumerator target, string propertyName)
{
return target.FirstOrDefault<JsonProperty>((Func<JsonProperty, bool>)(t => t.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase))).Value.ToString() ?? string.Empty;
}
}

View File

@@ -8,5 +8,4 @@
// QQ群605534569
//------------------------------------------------------------------------------
global using ThingsGateway;
global using ThingsGateway.NewLife.Extension;

View File

@@ -20,6 +20,7 @@ using ThingsGateway.NewLife;
using ThingsGateway.NewLife.Caching;
using ThingsGateway.NewLife.Threading;
using ThingsGateway.Schedule;
using ThingsGateway.SqlSugar;
namespace ThingsGateway.Admin.Application;
@@ -52,16 +53,16 @@ public class HardwareJob : IJob, IHardwareJob
#endregion
private MemoryCache MemoryCache = new() { };
private const string CacheKey = "HistoryHardwareInfo";
private ICache MemoryCache => App.CacheService;
private const string CacheKey = $"{CacheConst.Cache_HardwareInfo}HistoryHardwareInfo";
/// <inheritdoc/>
public async Task<List<HistoryHardwareInfo>> GetHistoryHardwareInfos()
{
var historyHardwareInfos = MemoryCache.Get<List<HistoryHardwareInfo>>(CacheKey);
if (historyHardwareInfos == null)
{
using var db = DbContext.Db.GetConnectionScopeWithAttr<HistoryHardwareInfo>().CopyNew();
historyHardwareInfos = await db.Queryable<HistoryHardwareInfo>().Where(a => a.Date > DateTime.Now.AddDays(-3)).ToListAsync().ConfigureAwait(false);
using var db = DbContext.GetDB<HistoryHardwareInfo>(); ;
historyHardwareInfos = await db.Queryable<HistoryHardwareInfo>().Where(a => a.Date > DateTime.Now.AddDays(-3)).Take(1000).ToListAsync().ConfigureAwait(false);
MemoryCache.Set(CacheKey, historyHardwareInfos);
}
@@ -70,6 +71,7 @@ public class HardwareJob : IJob, IHardwareJob
private bool error = false;
private DateTime hisInsertTime = default;
private SqlSugarClient _db = DbContext.GetDB<HistoryHardwareInfo>();
public async Task ExecuteAsync(JobExecutingContext context, CancellationToken stoppingToken)
{
@@ -79,8 +81,7 @@ public class HardwareJob : IJob, IHardwareJob
{
if (HardwareInfo.MachineInfo == null)
{
await MachineInfo.RegisterAsync().ConfigureAwait(false);
HardwareInfo.MachineInfo = MachineInfo.Current;
HardwareInfo.MachineInfo = MachineInfo.GetCurrent();
string currentPath = Directory.GetCurrentDirectory();
DriveInfo drive = new(Path.GetPathRoot(currentPath));
@@ -121,7 +122,6 @@ public class HardwareJob : IJob, IHardwareJob
if (DateTime.Now > hisInsertTime.Add(TimeSpan.FromMilliseconds(HardwareInfoOptions.HistoryInterval)))
{
hisInsertTime = DateTime.Now;
using var db = DbContext.Db.GetConnectionScopeWithAttr<HistoryHardwareInfo>().CopyNew();
{
var his = new HistoryHardwareInfo()
{
@@ -132,12 +132,12 @@ public class HardwareJob : IJob, IHardwareJob
CpuUsage = (HardwareInfo.MachineInfo.CpuRate * 100).ToInt(),
Temperature = (HardwareInfo.MachineInfo.Temperature).ToInt(),
};
await db.Insertable(his).ExecuteCommandAsync(stoppingToken).ConfigureAwait(false);
await _db.Insertable(his).ExecuteCommandAsync(stoppingToken).ConfigureAwait(false);
MemoryCache.Remove(CacheKey);
}
var sevenDaysAgo = TimerX.Now.AddDays(-HardwareInfoOptions.DaysAgo);
//删除特定信息
var result = await db.Deleteable<HistoryHardwareInfo>(a => a.Date <= sevenDaysAgo).ExecuteCommandAsync(stoppingToken).ConfigureAwait(false);
var result = await _db.Deleteable<HistoryHardwareInfo>(a => a.Date <= sevenDaysAgo).ExecuteCommandAsync(stoppingToken).ConfigureAwait(false);
if (result > 0)
{
MemoryCache.Remove(CacheKey);

View File

@@ -27,7 +27,7 @@ public class LogJob : IJob
private static async Task DeleteSysOperateLog(int daysAgo, CancellationToken stoppingToken)
{
using var db = DbContext.Db.GetConnectionScopeWithAttr<SysOperateLog>().CopyNew();
using var db = DbContext.GetDB<SysOperateLog>();
var time = DateTime.Now.AddDays(-daysAgo);
await db.DeleteableWithAttr<SysOperateLog>().Where(u => u.OpTime < time).ExecuteCommandAsync(stoppingToken).ConfigureAwait(false); // 删除操作日志
}

View File

@@ -143,7 +143,7 @@ public class DatabaseLoggingWriter : IDatabaseLoggingWriter
if (flush)
{
SqlSugarClient ??= DbContext.Db.GetConnectionScopeWithAttr<SysOperateLog>().CopyNew();
SqlSugarClient ??= DbContext.GetDB<SysOperateLog>();
await SqlSugarClient.InsertableWithAttr(_operateLogMessageQueue.ToListWithDequeue()).ExecuteCommandAsync().ConfigureAwait(false);//入库
return true;
}
@@ -202,7 +202,7 @@ public class DatabaseLoggingWriter : IDatabaseLoggingWriter
if (flush)
{
SqlSugarClient ??= DbContext.Db.GetConnectionScopeWithAttr<SysOperateLog>().CopyNew();
SqlSugarClient ??= DbContext.GetDB<SysOperateLog>();
await SqlSugarClient.InsertableWithAttr(_operateLogMessageQueue.ToListWithDequeue()).ExecuteCommandAsync().ConfigureAwait(false);//入库
return true;
}

View File

@@ -0,0 +1,33 @@
//------------------------------------------------------------------------------
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
// 此代码版权除特别声明外的代码归作者本人Diego所有
// 源代码使用协议遵循本仓库的开源协议及附加协议
// Gitee源代码仓库https://gitee.com/diego2098/ThingsGateway
// Github源代码仓库https://github.com/kimdiego2098/ThingsGateway
// 使用文档https://thingsgateway.cn/
// QQ群605534569
//------------------------------------------------------------------------------
using BootstrapBlazor.Components;
using Riok.Mapperly.Abstractions;
namespace ThingsGateway.Admin.Application;
[Mapper(UseDeepCloning = true, EnumMappingStrategy = EnumMappingStrategy.ByName, RequiredMappingStrategy = RequiredMappingStrategy.None)]
public static partial class AdminMapper
{
public static partial LoginInput AdaptLoginInput(this OpenApiLoginInput src);
public static partial OpenApiLoginOutput AdaptOpenApiLoginOutput(this LoginOutput src);
public static partial SessionOutput AdaptSessionOutput(this SysUser src);
public static partial SysUser AdaptSysUser(this SysUser src);
public static partial UserSelectorOutput AdaptUserSelectorOutput(this SysUser src);
public static partial List<SysResource> AdaptListSysResource(this IEnumerable<SysResource> src);
public static partial AppConfig AdaptAppConfig(this AppConfig src);
public static partial WorkbenchInfo AdaptWorkbenchInfo(this WorkbenchInfo src);
public static partial QueryData<UserSelectorOutput> AdaptQueryDataUserSelectorOutput(this QueryData<SysUser> src);
public static partial LoginInput AdaptLoginInput(this LoginInput src);
}

View File

@@ -1,18 +1,14 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Collections.Concurrent;
using System.Net.Http.Headers;
using System.Security.Claims;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Json;
using ThingsGateway.Extension;
@@ -50,7 +46,7 @@ public class AdminOAuthHandler<TOptions>(
/// </summary>
private static async Task Insertable()
{
var db = DbContext.Db.GetConnectionScopeWithAttr<SysOperateLog>().CopyNew();
var db = DbContext.GetDB<SysOperateLog>();
var appLifetime = App.RootServices!.GetService<IHostApplicationLifetime>()!;
while (!appLifetime.ApplicationStopping.IsCancellationRequested)
{
@@ -80,6 +76,7 @@ public class AdminOAuthHandler<TOptions>(
AuthenticationProperties properties,
OAuthTokenResponse tokens)
{
Backchannel.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", tokens.AccessToken);
properties.RedirectUri = Options.HomePath;
properties.IsPersistent = true;
var appConfig = await configService.GetAppConfigAsync().ConfigureAwait(false);
@@ -90,7 +87,7 @@ public class AdminOAuthHandler<TOptions>(
properties.ExpiresUtc = TimeProvider.System.GetUtcNow().AddSeconds(result);
expire = (int)(result / 60.0);
}
var user = await HandleUserInfoAsync(tokens).ConfigureAwait(false);
var user = await Options.HandleUserInfoAsync(Context, tokens).ConfigureAwait(false);
var loginEvent = await GetLogin(expire).ConfigureAwait(false);
await UpdateUser(loginEvent).ConfigureAwait(false);
@@ -148,43 +145,8 @@ public class AdminOAuthHandler<TOptions>(
}
/// <summary>处理用户信息方法</summary>
protected virtual async Task<JsonElement> HandleUserInfoAsync(OAuthTokenResponse tokens)
{
var request = new HttpRequestMessage(HttpMethod.Get, BuildUserInfoUrl(tokens));
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var response = await Backchannel.SendAsync(request, Context.RequestAborted).ConfigureAwait(false);
var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
if (response.IsSuccessStatusCode)
{
return JsonDocument.Parse(content).RootElement;
}
throw new OAuthTokenException($"OAuth user info endpoint failure: {await Display(response).ConfigureAwait(false)}");
}
/// <summary>生成用户信息请求地址方法</summary>
protected virtual string BuildUserInfoUrl(OAuthTokenResponse tokens)
{
return QueryHelpers.AddQueryString(Options.UserInformationEndpoint, new Dictionary<string, string>
{
{ "access_token", tokens.AccessToken }
});
}
/// <summary>生成错误信息方法</summary>
protected static async Task<string> Display(HttpResponseMessage response)
{
var output = new StringBuilder();
output.Append($"Status: {response.StatusCode}; ");
output.Append($"Headers: {response.Headers}; ");
output.Append($"Body: {await response.Content.ReadAsStringAsync().ConfigureAwait(false)};");
return output.ToString();
}
private async Task<LoginEvent> GetLogin(int expire)
{
@@ -247,7 +209,7 @@ public class AdminOAuthHandler<TOptions>(
#endregion ,
using var db = DbContext.Db.GetConnectionScopeWithAttr<SysUser>().CopyNew();
using var db = DbContext.GetDB<SysUser>();
//更新用户登录信息
if (await db.Updateable(sysUser).UpdateColumns(it => new
{

View File

@@ -0,0 +1,87 @@
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
namespace ThingsGateway.Admin.Application;
/// <summary>OAuthOptions 配置类</summary>
public abstract class AdminOAuthOptions : OAuthOptions
{
/// <summary>默认构造函数</summary>
protected AdminOAuthOptions()
{
ConfigureClaims();
this.Events.OnRemoteFailure = context =>
{
var redirectUri = string.IsNullOrEmpty(HomePath) ? "/" : HomePath;
context.Response.Redirect(redirectUri);
context.HandleResponse();
return Task.CompletedTask;
};
Backchannel = new HttpClient(new HttpClientHandler
{
ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
});
Backchannel.DefaultRequestHeaders.UserAgent.Add(
new ProductInfoHeaderValue("ThingsGateway", "1.0"));
}
/// <summary>配置 Claims 映射</summary>
protected virtual void ConfigureClaims()
{
}
public virtual string GetName(JsonElement element)
{
JsonElement.ObjectEnumerator target = element.EnumerateObject();
return target.TryGetValue("name");
}
/// <summary>获得/设置 登陆后首页</summary>
public string HomePath { get; set; } = "/";
/// <summary>处理用户信息方法</summary>
public virtual async Task<JsonElement> HandleUserInfoAsync(HttpContext context, OAuthTokenResponse tokens)
{
var request = new HttpRequestMessage(HttpMethod.Get, BuildUserInfoUrl(tokens));
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var response = await Backchannel.SendAsync(request, context.RequestAborted).ConfigureAwait(false);
var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
if (response.IsSuccessStatusCode)
{
return JsonDocument.Parse(content).RootElement;
}
throw new OAuthTokenException($"OAuth user info endpoint failure: {await Display(response).ConfigureAwait(false)}");
}
/// <summary>生成用户信息请求地址方法</summary>
protected virtual string BuildUserInfoUrl(OAuthTokenResponse tokens)
{
return QueryHelpers.AddQueryString(UserInformationEndpoint, new Dictionary<string, string>
{
{ "access_token", tokens.AccessToken }
});
}
/// <summary>生成错误信息方法</summary>
protected async Task<string> Display(HttpResponseMessage response)
{
var output = new StringBuilder();
output.Append($"Status: {response.StatusCode}; ");
output.Append($"Headers: {response.Headers}; ");
output.Append($"Body: {await response.Content.ReadAsStringAsync().ConfigureAwait(false)};");
return output.ToString();
}
}

View File

@@ -3,16 +3,20 @@ using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.AspNetCore.WebUtilities;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using ThingsGateway.NewLife.Log;
namespace ThingsGateway.Admin.Application;
public class GiteeOAuthOptions : AdminOAuthOptions
{
INoticeService _noticeService;
IVerificatInfoService _verificatInfoService;
public GiteeOAuthOptions() : base()
{
_noticeService = App.GetService<INoticeService>();
_verificatInfoService = App.GetService<IVerificatInfoService>();
this.SignInScheme = ClaimConst.Scheme;
this.AuthorizationEndpoint = "https://gitee.com/oauth/authorize";
this.TokenEndpoint = "https://gitee.com/oauth/token";
@@ -29,11 +33,14 @@ public class GiteeOAuthOptions : AdminOAuthOptions
Events.OnRedirectToAuthorizationEndpoint = context =>
{
//context.RedirectUri = context.RedirectUri.Replace("http%3A%2F%2F", "https%3A%2F%2F"); // 强制替换
context.Response.Redirect(context.RedirectUri);
return Task.CompletedTask;
};
Events.OnRemoteFailure = context =>
{
XTrace.WriteException(context.Failure);
return Task.CompletedTask;
};
}
/// <summary>刷新 Token 方法</summary>
@@ -60,16 +67,7 @@ public class GiteeOAuthOptions : AdminOAuthOptions
return OAuthTokenResponse.Failed(new OAuthTokenException($"OAuth token endpoint failure: {await Display(response).ConfigureAwait(false)}"));
}
/// <summary>生成错误信息方法</summary>
protected static async Task<string> Display(HttpResponseMessage response)
{
var output = new StringBuilder();
output.Append($"Status: {response.StatusCode}; ");
output.Append($"Headers: {response.Headers}; ");
output.Append($"Body: {await response.Content.ReadAsStringAsync().ConfigureAwait(false)};");
return output.ToString();
}
public override string GetName(JsonElement element)
{
@@ -77,7 +75,7 @@ public class GiteeOAuthOptions : AdminOAuthOptions
return target.TryGetValue("name");
}
private static async Task HandlerGiteeStarredUrl(OAuthCreatingTicketContext context, string repoFullName = "ThingsGateway/ThingsGateway")
private async Task HandlerGiteeStarredUrl(OAuthCreatingTicketContext context, string repoFullName = "ThingsGateway/ThingsGateway")
{
if (string.IsNullOrWhiteSpace(context.AccessToken))
throw new InvalidOperationException("Access token is missing.");
@@ -89,7 +87,7 @@ public class GiteeOAuthOptions : AdminOAuthOptions
{ "access_token", context.AccessToken }
};
var request = new HttpRequestMessage(HttpMethod.Put, QueryHelpers.AddQueryString(uri, queryString))
var request = new HttpRequestMessage(HttpMethod.Get, QueryHelpers.AddQueryString(uri, queryString))
{
Headers = { Accept = { new MediaTypeWithQualityHeaderValue("application/json") } }
};
@@ -99,7 +97,17 @@ public class GiteeOAuthOptions : AdminOAuthOptions
if (!response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
throw new Exception($"Failed to star repository: {response.StatusCode}, {content}");
var id = context.Identity.Claims.FirstOrDefault(a => a.Type == ClaimConst.VerificatId).Value;
var verificatInfoIds = _verificatInfoService.GetOne(id.ToLong());
_ = Task.Run(async () =>
{
await Task.Delay(5000).ConfigureAwait(false);
await _noticeService.NavigationMesage(verificatInfoIds.ClientIds, "https://gitee.com/ThingsGateway/ThingsGateway", "创作不易如有帮助请star仓库").ConfigureAwait(false);
});
//throw new Exception($"Failed to star repository: {response.StatusCode}, {content}");
}

View File

@@ -0,0 +1,122 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OAuth;
using Microsoft.AspNetCore.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using ThingsGateway.NewLife.Log;
namespace ThingsGateway.Admin.Application;
public class GitHubOAuthOptions : AdminOAuthOptions
{
INoticeService _noticeService;
IVerificatInfoService _verificatInfoService;
public GitHubOAuthOptions() : base()
{
_noticeService = App.GetService<INoticeService>();
_verificatInfoService = App.GetService<IVerificatInfoService>();
SignInScheme = ClaimConst.Scheme;
AuthorizationEndpoint = "https://github.com/login/oauth/authorize";
TokenEndpoint = "https://github.com/login/oauth/access_token";
UserInformationEndpoint = "https://api.github.com/user";
HomePath = "/";
CallbackPath = "/signin-github";
Scope.Add("read:user");
Scope.Add("public_repo"); // 需要用于 Star 仓库
Events.OnCreatingTicket = async context =>
{
await HandleGitHubStarAsync(context).ConfigureAwait(false);
};
Events.OnRedirectToAuthorizationEndpoint = context =>
{
context.Response.Redirect(context.RedirectUri);
return Task.CompletedTask;
};
Events.OnRemoteFailure = context =>
{
XTrace.WriteException(context.Failure);
return Task.CompletedTask;
};
}
protected override void ConfigureClaims()
{
ClaimActions.MapJsonKey(ClaimConst.AvatarUrl, "avatar_url");
ClaimActions.MapJsonKey(ClaimConst.Account, "login");
base.ConfigureClaims();
}
public override string GetName(JsonElement element)
{
if (element.TryGetProperty("login", out var loginProp))
{
return loginProp.GetString() ?? string.Empty;
}
return string.Empty;
}
private async Task HandleGitHubStarAsync(OAuthCreatingTicketContext context, string repoFullName = "ThingsGateway/ThingsGateway")
{
if (string.IsNullOrWhiteSpace(context.AccessToken))
throw new InvalidOperationException("Access token is missing.");
var request = new HttpRequestMessage(HttpMethod.Put, $"https://api.github.com/user/starred/{repoFullName}")
{
Headers =
{
Accept = { new MediaTypeWithQualityHeaderValue("application/vnd.github+json") },
Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken),
},
Content = new StringContent(string.Empty) // GitHub Star 接口需要空内容
};
request.Headers.UserAgent.Add(new ProductInfoHeaderValue("ThingsGateway", "1.0")); // GitHub API 要求 User-Agent
var response = await context.Backchannel.SendAsync(request, context.HttpContext.RequestAborted).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
var id = context.Identity.Claims.FirstOrDefault(a => a.Type == ClaimConst.VerificatId).Value;
var verificatInfoIds = _verificatInfoService.GetOne(id.ToLong());
_ = Task.Run(async () =>
{
await Task.Delay(5000).ConfigureAwait(false);
await _noticeService.NavigationMesage(verificatInfoIds.ClientIds, "https://github.com/ThingsGateway/ThingsGateway", "创作不易如有帮助请star仓库").ConfigureAwait(false);
});
}
}
/// <summary>处理用户信息方法</summary>
public override async Task<JsonElement> HandleUserInfoAsync(HttpContext context, OAuthTokenResponse tokens)
{
var request = new HttpRequestMessage(HttpMethod.Get, UserInformationEndpoint);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github+json"));
request.Headers.UserAgent.Add(new ProductInfoHeaderValue("ThingsGateway", "1.0")); // GitHub API 要求 User-Agent
var response = await Backchannel.SendAsync(request, context.RequestAborted).ConfigureAwait(false);
var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
if (response.IsSuccessStatusCode)
{
return JsonDocument.Parse(content).RootElement;
}
throw new OAuthTokenException($"OAuth user info endpoint failure: {await Display(response).ConfigureAwait(false)}");
}
}

View File

@@ -0,0 +1,6 @@
namespace ThingsGateway.Admin.Application;
public class GithubOAuthSettings : GiteeOAuthSettings
{
}

View File

@@ -0,0 +1,11 @@
using System.Text.Json;
namespace ThingsGateway.Admin.Application;
public static class OAuthUserExtensions
{
public static string TryGetValue(this JsonElement.ObjectEnumerator target, string propertyName)
{
return target.FirstOrDefault<JsonProperty>((Func<JsonProperty, bool>)(t => t.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase))).Value.ToString() ?? string.Empty;
}
}

View File

@@ -18,7 +18,7 @@ public class SysRelationSeedData : ISqlSugarEntitySeedData<SysRelation>
/// <inheritdoc/>
public IEnumerable<SysRelation> SeedData()
{
var db = DbContext.Db.GetConnectionScopeWithAttr<SysRelation>().CopyNew();
using var db = DbContext.GetDB<SysRelation>();
if (db.Queryable<SysRelation>().Any(a => a.ObjectId == RoleConst.SuperAdminId))
return Enumerable.Empty<SysRelation>();
var data = SeedDataUtil.GetSeedData<SysRelation>(PathExtensions.CombinePathWithOs("SeedData", "Admin", "seed_sys_relation.json"));

View File

@@ -324,7 +324,7 @@ public class AuthService : IAuthService
#endregion ,
using var db = DbContext.Db.GetConnectionScopeWithAttr<SysUser>().CopyNew();
using var db = DbContext.GetDB<SysUser>();
//更新用户登录信息
if (await db.Updateable(sysUser).UpdateColumns(it => new
{

View File

@@ -10,6 +10,8 @@
using System.Collections.Concurrent;
using ThingsGateway.NewLife.DictionaryExtensions;
namespace ThingsGateway.Admin.Application;
/// <summary>

View File

@@ -12,8 +12,6 @@ using BootstrapBlazor.Components;
using Microsoft.Extensions.DependencyInjection;
using System.Globalization;
using ThingsGateway.FriendlyException;
using ThingsGateway.SqlSugar;
@@ -23,7 +21,7 @@ internal sealed class SysResourceService : BaseService<SysResource>, ISysResourc
{
private readonly IRelationService _relationService;
private string CacheKey = $"{CacheConst.Cache_SysResource}-{CultureInfo.CurrentUICulture.Name}";
private string CacheKey = $"{CacheConst.Cache_SysResource}";
public SysResourceService(IRelationService relationService)
{
@@ -32,7 +30,6 @@ internal sealed class SysResourceService : BaseService<SysResource>, ISysResourc
#region
[OperDesc("CopyResource")]
public async Task CopyAsync(IEnumerable<long> ids, long moduleId)
{
@@ -143,12 +140,12 @@ internal sealed class SysResourceService : BaseService<SysResource>, ISysResourc
/// <returns>全部资源列表</returns>
public async Task<List<SysResource>> GetAllAsync()
{
var sysResources = App.CacheService.Get<List<SysResource>>(CacheKey);
var sysResources = App.CacheService.Get<List<SysResource>>(CacheConst.Cache_SysResource);
if (sysResources == null)
{
using var db = GetDB();
sysResources = await db.Queryable<SysResource>().ToListAsync().ConfigureAwait(false);
App.CacheService.Set(CacheKey, sysResources);
App.CacheService.Set(CacheConst.Cache_SysResource, sysResources);
}
return sysResources;
}
@@ -258,7 +255,7 @@ internal sealed class SysResourceService : BaseService<SysResource>, ISysResourc
/// </summary>
public void RefreshCache()
{
App.CacheService.Remove(CacheKey);
App.CacheService.Remove(CacheConst.Cache_SysResource);
//删除超级管理员的缓存
App.RootServices.GetRequiredService<ISysUserService>().DeleteUserFromCache(RoleConst.SuperAdminId);
}

View File

@@ -10,8 +10,6 @@
using BootstrapBlazor.Components;
using Mapster;
using ThingsGateway.SqlSugar;
namespace ThingsGateway.Admin.Application;
@@ -69,7 +67,7 @@ internal sealed class SessionService : BaseService<SysUser>, ISessionService
var r = items.Select((it) =>
{
var reuslt = it.Adapt<SessionOutput>();
var reuslt = it.AdaptSessionOutput();
if (verificatInfoDicts.TryGetValue(it.Id, out var verificatInfos))
{
reuslt.VerificatCount = verificatInfos.Count;//令牌数量
@@ -94,7 +92,7 @@ internal sealed class SessionService : BaseService<SysUser>, ISessionService
var r = items.Select((it) =>
{
var reuslt = it.Adapt<SessionOutput>();
var reuslt = it.AdaptSessionOutput();
if (verificatInfoDicts.TryGetValue(it.Id, out var verificatInfos))
{
reuslt.VerificatCount = verificatInfos.Count;//令牌数量
@@ -117,7 +115,7 @@ internal sealed class SessionService : BaseService<SysUser>, ISessionService
var r = items.Select((it) =>
{
var reuslt = it.Adapt<SessionOutput>();
var reuslt = it.AdaptSessionOutput();
if (verificatInfoDicts.TryGetValue(it.Id, out var verificatInfos))
{
reuslt.VerificatCount = verificatInfos.Count;//令牌数量

View File

@@ -10,8 +10,6 @@
using BootstrapBlazor.Components;
using Mapster;
using ThingsGateway.DataEncryption;
using ThingsGateway.Extension;
using ThingsGateway.Extension.Generic;
@@ -452,7 +450,7 @@ internal sealed class SysUserService : BaseService<SysUser>, ISysUserService
if (changedType == ItemChangedType.Add)
{
var sysUser = input.Adapt<SysUser>();
var sysUser = input.AdaptSysUser();
//获取默认密码
sysUser.Avatar = input.Avatar;
sysUser.Password = await GetDefaultPassWord(true).ConfigureAwait(false);//设置密码
@@ -872,7 +870,7 @@ internal sealed class SysUserService : BaseService<SysUser>, ISysUserService
sysUser.OrgAndPosIdList.AddRange(sysUser.OrgId, sysUser.PositionId ?? 0);//添加组织和职位Id
if (sysUser.DirectorId != null)
{
sysUser.DirectorInfo = (await GetUserByIdAsync(sysUser.DirectorId.Value).ConfigureAwait(false)).Adapt<UserSelectorOutput>();//获取主管信息
sysUser.DirectorInfo = (await GetUserByIdAsync(sysUser.DirectorId.Value).ConfigureAwait(false)).AdaptUserSelectorOutput();//获取主管信息
}
//获取按钮码

View File

@@ -10,11 +10,13 @@
using BootstrapBlazor.Components;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Reflection;
using System.Text;
using ThingsGateway.Extension;
using ThingsGateway.UnifyResult;
namespace ThingsGateway.Admin.Application;
@@ -65,10 +67,77 @@ public class Startup : AppStartup
services.AddSingleton(typeof(IEventService<>), typeof(EventService<>));
#region
services.AddConsoleFormatter(options =>
{
options.WriteFilter = (logMsg) =>
{
if (App.HostApplicationLifetime.ApplicationStopping.IsCancellationRequested && logMsg.LogLevel >= LogLevel.Warning) return false;
if (string.IsNullOrEmpty(logMsg.Message)) return false;
else return true;
};
options.MessageFormat = (logMsg) =>
{
//如果不是LoggingMonitor日志才格式化
if (logMsg.LogName != "System.Logging.LoggingMonitor")
{
var stringBuilder = new StringBuilder();
stringBuilder.AppendLine("【日志级别】:" + logMsg.LogLevel);
stringBuilder.AppendLine("【日志类名】:" + logMsg.LogName);
stringBuilder.AppendLine("【日志时间】:" + DateTime.Now.ToDefaultDateTimeFormat());
stringBuilder.AppendLine("【日志内容】:" + logMsg.Message);
if (logMsg.Exception != null)
{
stringBuilder.AppendLine("【异常信息】:" + logMsg.Exception);
}
return stringBuilder.ToString();
}
else
{
return logMsg.Message;
}
};
options.WriteHandler = (logMsg, scopeProvider, writer, fmtMsg, opt) =>
{
ConsoleColor consoleColor = ConsoleColor.White;
switch (logMsg.LogLevel)
{
case LogLevel.Information:
consoleColor = ConsoleColor.DarkGreen;
break;
case LogLevel.Warning:
consoleColor = ConsoleColor.DarkYellow;
break;
case LogLevel.Error:
consoleColor = ConsoleColor.DarkRed;
break;
}
writer.WriteWithColor(fmtMsg, ConsoleColor.Black, consoleColor);
};
});
#endregion
//日志写入数据库配置
services.AddDatabaseLogging<DatabaseLoggingWriter>(options =>
{
options.NameFilter = (name) =>
{
return (
name == "System.Logging.RequestAudit"
);
};
});
}
public void Use(IApplicationBuilder applicationBuilder)
public void Use(IServiceProvider serviceProvider)
{
NewLife.Log.XTrace.UnhandledExceptionLogEnable = () => !App.HostApplicationLifetime.ApplicationStopping.IsCancellationRequested;
//检查ConfigId
var configIdGroup = DbContext.DbConfigs.GroupBy(it => it.ConfigId);
foreach (var configId in configIdGroup)
@@ -79,7 +148,7 @@ public class Startup : AppStartup
//遍历配置
DbContext.DbConfigs?.ForEach(it =>
{
var connection = DbContext.Db.GetConnection(it.ConfigId);//获取数据库连接对象
var connection = DbContext.GetDB().GetConnection(it.ConfigId);//获取数据库连接对象
if (it.InitDatabase == true)
connection.DbMaintenance.CreateDatabase();//创建数据库,如果存在则不创建
});

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="$(SolutionDir)Version.props" />
<Import Project="$(SolutionDir)PackNuget.props" />
<Import Project="..\..\Version.props" />
<Import Project="..\..\PackNuget.props" />
<PropertyGroup>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
@@ -18,7 +18,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Rougamo.Fody" Version="5.0.0" />
<PackageReference Include="Riok.Mapperly" Version="4.2.1" ExcludeAssets="runtime" PrivateAssets="all" />
<PackageReference Include="Rougamo.Fody" Version="5.0.1" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.1" />
@@ -41,13 +42,20 @@
<ItemGroup>
<None Include="..\README.md" Pack="true" PackagePath="\" />
<None Include="..\README.zh-CN.md" Pack="true" PackagePath="\" />
<None Remove="$(SolutionDir)..\README.md" Pack="false" PackagePath="\" />
<None Remove="$(SolutionDir)..\README.zh-CN.md" Pack="false" PackagePath="\" />
<None Remove="..\..\..\README.md" Pack="false" PackagePath="\" />
<None Remove="..\..\..\README.zh-CN.md" Pack="false" PackagePath="\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ThingsGateway.Razor\ThingsGateway.Razor.csproj" />
<ProjectReference Include="..\ThingsGateway.DB\ThingsGateway.DB.csproj" />
</ItemGroup>
<!--<Target Name="Mapster" AfterTargets="AfterBuild">
<Exec WorkingDirectory="$(ProjectDir)" Command="dotnet tool restore" />
<Exec WorkingDirectory="$(ProjectDir)" Command="dotnet mapster extension -o MapsterGenerator -a &quot;$(TargetDir)$(ProjectName).dll&quot;" />
<Exec WorkingDirectory="$(ProjectDir)" Command="dotnet mapster mapper -o MapsterGenerator -a &quot;$(TargetDir)$(ProjectName).dll&quot;" />
</Target>-->
</Project>

View File

@@ -21,7 +21,7 @@ namespace ThingsGateway.Admin.Application
Settings = new UserAgentSettings();
}
private MemoryCache MemoryCache { get; set; } = new();
private ICache MemoryCache => App.CacheService;
/// <summary>
/// Parses the specified user agent string.

View File

@@ -40,7 +40,7 @@ public static class ClearTokenUtil
public static async Task DeleteUserTokenByOrgIds(HashSet<long> orgIds)
{
// 获取用户ID列表
var userIds = await DbContext.Db.CopyNew().QueryableWithAttr<SysUser>().Where(it => orgIds.Contains(it.OrgId)).Select(it => it.Id).ToListAsync().ConfigureAwait(false);
var userIds = await DbContext.GetDB<SysUser>().Queryable<SysUser>().Where(it => orgIds.Contains(it.OrgId)).Select(it => it.Id).ToListAsync().ConfigureAwait(false);
//从redis中删除所属机构的用户token
App.CacheService.HashDel<VerificatInfo>(CacheConst.Cache_Token, userIds.Select(it => it.ToString()).ToArray());
}

View File

@@ -5,6 +5,9 @@
@<div>
<span class="mx-3">@item.ConfirmMessage</span>
<Button Text=@Localizers["Jump"] Color="Color.Link" OnClick="()=>NavigationManager.NavigateTo(item.Uri)"></Button>
<a href=@item.Uri target="_blank">
@item.Uri
</a>
</div>;
}

View File

@@ -8,8 +8,6 @@
// QQ群605534569
//------------------------------------------------------------------------------
using Mapster;
using ThingsGateway.Admin.Application;
using ThingsGateway.NewLife;
using ThingsGateway.NewLife.Extension;
@@ -88,7 +86,7 @@ public class BlazorAppContext
if (UserManager.UserId > 0)
{
url = url.StartsWith('/') ? url : $"/{url}";
var sysResources = (await ResourceService.GetAllAsync()).Adapt<List<SysResource>>();
var sysResources = (await ResourceService.GetAllAsync()).AdaptListSysResource();
if (TitleLocalizer != null)
{
sysResources.ForEach(a =>
@@ -121,7 +119,7 @@ public class BlazorAppContext
CurrentModuleId = moduleId.Value;
}
UserWorkBench = await UserCenterService.GetLoginWorkbenchAsync(UserManager.UserId);
OwnMenus = (await UserCenterService.GetOwnMenuAsync(UserManager.UserId, 0)).Adapt<List<SysResource>>();
OwnMenus = (await UserCenterService.GetOwnMenuAsync(UserManager.UserId, 0)).AdaptListSysResource();
if (TitleLocalizer != null)
{
@@ -156,7 +154,7 @@ public class BlazorAppContext
CurrentUser = (await SysUserService.GetUserByIdAsync(UserManager.UserId))!;
}
}
TimeTick timeTick = new("50000");
TimeTick timeTick = new("60000");
/// <summary>
/// 是否拥有按钮授权
/// </summary>

View File

@@ -19,3 +19,4 @@ global using System.Diagnostics.CodeAnalysis;
global using ThingsGateway.Razor;
[assembly: SuppressMessage("Reliability", "CA2007", Justification = "<挂起>", Scope = "module")]
[assembly: GlobalGenerateSetParametersAsync(true)]

View File

@@ -8,8 +8,6 @@
// QQ群605534569
//------------------------------------------------------------------------------
using Mapster;
using Microsoft.AspNetCore.Components.Forms;
using ThingsGateway.Admin.Application;
@@ -30,7 +28,7 @@ public partial class AppConfigPage
protected override async Task OnParametersSetAsync()
{
AppConfig = (await SysDictService.GetAppConfigAsync()).Adapt<AppConfig>();
AppConfig = (await SysDictService.GetAppConfigAsync()).AdaptAppConfig();
await base.OnParametersSetAsync();
}

View File

@@ -64,6 +64,7 @@ public partial class HardwareInfoPage : IDisposable
private async Task<ChartDataSource> OnInit()
{
if (App.HostApplicationLifetime.ApplicationStopping.IsCancellationRequested) return (new ChartDataSource());
if (ChartDataSource == null)
{
var hisHardwareInfos = await HardwareJob.GetHistoryHardwareInfos();

View File

@@ -26,6 +26,7 @@ public partial class OperLogPage
private async Task<ChartDataSource> OnInit()
{
if (App.HostApplicationLifetime.ApplicationStopping.IsCancellationRequested) return (new ChartDataSource());
if (ChartDataSource == null)
{
var dayStatisticsOutputs = await SysOperateLogService.StatisticsByDayAsync(7);

View File

@@ -33,7 +33,7 @@
</PopConfirmButton>
<PopConfirmButton Color=Color.Warning IsDisabled="SelectedRows.Count!=1||!AuthorizeButton(AdminOperConst.Edit)" Text=@OperDescLocalizer["ChangeParentResource"] Icon="fa fa-copy" OnConfirm="OnChangeParent">
<BodyTemplate>
<div class="min-height-500 overflow-y-auto">
<div class="overflow-y-auto" style="height:500px">
<TreeView Items="MenuTreeItems" IsVirtualize="true" OnTreeItemClick="a=>{ChangeParentId=a.Value.Id;return Task.CompletedTask;}" />
</div>
</BodyTemplate>

View File

@@ -22,12 +22,11 @@ public partial class SessionPage
#region
private async Task<QueryData<SessionOutput>> OnQueryAsync(QueryPageOptions options)
private Task<QueryData<SessionOutput>> OnQueryAsync(QueryPageOptions options)
{
return await Task.Run(async () =>
return Task.Run(() =>
{
var data = await SessionService.PageAsync(options);
return data;
return SessionService.PageAsync(options);
});
}

View File

@@ -8,8 +8,6 @@
// QQ群605534569
//------------------------------------------------------------------------------
using Mapster;
using ThingsGateway.Admin.Application;
namespace ThingsGateway.Admin.Razor;
@@ -47,10 +45,7 @@ public partial class RoleChoiceDialog
private ISysRoleService? SysRoleService { get; set; }
private async Task<QueryData<SysRole>> OnQueryAsync(QueryPageOptions options)
{
var data = await SysRoleService.PageAsync(options, a => a.Where(b => b.OrgId == OrgId));
QueryData<SysRole> queryData = data.Adapt<QueryData<SysRole>>();
return queryData;
return await SysRoleService.PageAsync(options, a => a.Where(b => b.OrgId == OrgId));
}
#region
private long OrgId { get; set; }

View File

@@ -39,6 +39,7 @@ public partial class SysUserEdit
var items = await SysPositionService.SelectorAsync(new PositionSelectorInput() { });
Items = PositionUtil.BuildCascaderItemList(items);
ModuleSelectedItems = ResourceUtil.BuildModuleSelectList((await SysResourceService.GetAllAsync())).ToList();
await InvokeAsync(StateHasChanged);
await base.OnInitializedAsync();
}

View File

@@ -8,8 +8,6 @@
// QQ群605534569
//------------------------------------------------------------------------------
using Mapster;
using ThingsGateway.Admin.Application;
namespace ThingsGateway.Admin.Razor;
@@ -56,7 +54,7 @@ public partial class UserChoiceDialog
OrgId = OrgId,
PositionId = PositionId,
});
QueryData<UserSelectorOutput> queryData = data.Adapt<QueryData<UserSelectorOutput>>();
QueryData<UserSelectorOutput> queryData = data.AdaptQueryDataUserSelectorOutput();
return queryData;
}

View File

@@ -8,8 +8,6 @@
// QQ群605534569
//------------------------------------------------------------------------------
using Mapster;
using Microsoft.AspNetCore.Components.Forms;
using ThingsGateway.Admin.Application;
@@ -37,9 +35,9 @@ public partial class UserCenterPage
protected override async Task OnParametersSetAsync()
{
SysUser = AppContext.CurrentUser.Adapt<SysUser>();
SysUser = AppContext.CurrentUser.AdaptSysUser();
SysUser.Avatar = AppContext.Avatar;
WorkbenchInfo = (await UserCenterService.GetLoginWorkbenchAsync(SysUser.Id)).Adapt<WorkbenchInfo>();
WorkbenchInfo = (await UserCenterService.GetLoginWorkbenchAsync(SysUser.Id)).AdaptWorkbenchInfo();
await base.OnParametersSetAsync();
}

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">
<Import Project="$(SolutionDir)Version.props" />
<Import Project="$(SolutionDir)PackNuget.props" />
<Import Project="..\..\Version.props" />
<Import Project="..\..\PackNuget.props" />
<ItemGroup>
<ProjectReference Include="..\ThingsGateway.Admin.Application\ThingsGateway.Admin.Application.csproj" />
@@ -17,6 +17,7 @@
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<!--<UseRazorSourceGenerator>false</UseRazorSourceGenerator>-->
</PropertyGroup>
<ItemGroup>
<Content Remove="Locales\*.json" />
@@ -29,9 +30,15 @@
<ItemGroup>
<None Include="..\README.md" Pack="true" PackagePath="\" />
<None Include="..\README.zh-CN.md" Pack="true" PackagePath="\" />
<None Remove="$(SolutionDir)..\README.md" Pack="false" PackagePath="\" />
<None Remove="$(SolutionDir)..\README.zh-CN.md" Pack="false" PackagePath="\" />
<None Remove="..\..\..\README.md" Pack="false" PackagePath="\" />
<None Remove="..\..\..\README.zh-CN.md" Pack="false" PackagePath="\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BlazorSetParametersAsyncGenerator\BlazorSetParametersAsyncGenerator.csproj" PrivateAssets="all" OutputItemType="Analyzer" />
</ItemGroup>
</Project>

View File

@@ -8,8 +8,6 @@
// QQ群605534569
//------------------------------------------------------------------------------
using Mapster;
using ThingsGateway.Admin.Application;
namespace ThingsGateway.Admin.Razor;
@@ -122,31 +120,4 @@ public static class ResourceUtil
return trees;
}
/// <summary>
/// 构建树节点
/// </summary>
public static List<TreeViewItem<T>> BuildTreeItemList<T>(IEnumerable<SysResource> sysresources, List<long> selectedItems, Microsoft.AspNetCore.Components.RenderFragment<T> render, long parentId = 0, TreeViewItem<T>? parent = null, Func<SysResource, bool> disableFunc = null) where T : class
{
if (sysresources == null) return null;
var trees = new List<TreeViewItem<T>>();
var roots = sysresources.Where(i => i.ParentId == parentId).OrderBy(i => i.SortCode);
foreach (var node in roots)
{
var item = new TreeViewItem<T>(node.Adapt<T>())
{
Text = node.Title,
Icon = node.Icon,
IsDisabled = disableFunc == null ? false : disableFunc(node),
IsActive = selectedItems.Any(v => node.Id == v),
IsExpand = true,
Parent = parent,
Template = render,
CheckedState = selectedItems.Any(i => i == node.Id) ? CheckboxState.Checked : CheckboxState.UnChecked
};
item.Items = BuildTreeItemList(sysresources, selectedItems, render, node.Id, item, disableFunc) ?? new();
trees.Add(item);
}
return trees;
}
}

View File

@@ -14,6 +14,7 @@
"ThingsGateway.SqlSugar",
"ThingsGateway.Admin.Application",
"ThingsGateway.Admin.Razor",
"ThingsGateway.DB",
"ThingsGateway.Razor"
]
},

View File

@@ -14,6 +14,7 @@
"ThingsGateway.SqlSugar",
"ThingsGateway.Admin.Application",
"ThingsGateway.Admin.Razor",
"ThingsGateway.DB",
"ThingsGateway.Razor"
]
},

View File

@@ -0,0 +1,22 @@
{
"Cache": {
"CacheType": "Memory", // 可选Memory 或 Redis
"MemoryCacheOptions": {
"Expire": 3600,
"Capacity": 100000,
"Period": 60
},
"RedisCacheOptions": {
"InstanceName": "ThingsGateway",
"Configuration": "server=127.0.0.1:6379;password=123456;db=3;timeout=3000",
"Server": "127.0.0.1:6379",
"Db": 3,
"UserName": "",
"Password": "123456",
"Timeout": 3000,
"Prefix": "ThingsGateway:"
}
}
}

View File

@@ -11,8 +11,6 @@
#pragma warning disable CA2007 // 考虑对等待的任务调用 ConfigureAwait
using BootstrapBlazor.Components;
using Mapster;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.Extensions.Localization;
@@ -71,7 +69,7 @@ public partial class Login
private async Task LoginAsync(EditContext context)
{
var model = loginModel.Adapt<LoginInput>();
var model = loginModel.AdaptLoginInput();
model.Password = DESEncryption.Encrypt(model.Password);
try
{

View File

@@ -40,6 +40,7 @@ public class SingleFilePublish : ISingleFilePublish
"ThingsGateway.NewLife.X",
"ThingsGateway.Razor",
"ThingsGateway.Admin.Razor" ,
"ThingsGateway.DB",
"ThingsGateway.Admin.Application",
"ThingsGateway.SqlSugar",
];

View File

@@ -21,14 +21,12 @@ using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Unicode;
using ThingsGateway.Admin.Application;
using ThingsGateway.Admin.Razor;
using ThingsGateway.Extension;
using ThingsGateway.NewLife.Caching;
using ThingsGateway.VirtualFileServer;
namespace ThingsGateway.AdminServer;
@@ -48,11 +46,11 @@ public class Startup : AppStartup
});
// 事件总线
services.AddEventBus(options =>
{
//// 事件总线
//services.AddEventBus(options =>
//{
});
//});
// 任务调度
services.AddSchedule(options =>
@@ -60,8 +58,6 @@ public class Startup : AppStartup
options.AddPersistence<JobPersistence>();
});
// 缓存
services.AddSingleton<ICache, MemoryCache>();
// 允许跨域
services.AddCorsAccessor();
@@ -151,104 +147,10 @@ public class Startup : AppStartup
});
services.AddHealthChecks();
#region
services.AddConsoleFormatter(options =>
{
options.WriteFilter = (logMsg) =>
{
if (App.HostApplicationLifetime.ApplicationStopping.IsCancellationRequested && logMsg.LogLevel >= LogLevel.Warning) return false;
if (string.IsNullOrEmpty(logMsg.Message)) return false;
else return true;
};
options.MessageFormat = (logMsg) =>
{
//如果不是LoggingMonitor日志才格式化
if (logMsg.LogName != "System.Logging.LoggingMonitor")
{
var stringBuilder = new StringBuilder();
stringBuilder.AppendLine("【日志级别】:" + logMsg.LogLevel);
stringBuilder.AppendLine("【日志类名】:" + logMsg.LogName);
stringBuilder.AppendLine("【日志时间】:" + DateTime.Now.ToDefaultDateTimeFormat());
stringBuilder.AppendLine("【日志内容】:" + logMsg.Message);
if (logMsg.Exception != null)
{
stringBuilder.AppendLine("【异常信息】:" + logMsg.Exception);
}
return stringBuilder.ToString();
}
else
{
return logMsg.Message;
}
};
options.WriteHandler = (logMsg, scopeProvider, writer, fmtMsg, opt) =>
{
ConsoleColor consoleColor = ConsoleColor.White;
switch (logMsg.LogLevel)
{
case LogLevel.Information:
consoleColor = ConsoleColor.DarkGreen;
break;
case LogLevel.Warning:
consoleColor = ConsoleColor.DarkYellow;
break;
case LogLevel.Error:
consoleColor = ConsoleColor.DarkRed;
break;
}
writer.WriteWithColor(fmtMsg, ConsoleColor.Black, consoleColor);
};
});
#endregion
#region api日志
//Monitor日志配置
//services.AddMonitorLogging(options =>
//{
// options.JsonIndented = true;// 是否美化 JSON
// options.GlobalEnabled = false;//全局启用
// options.ConfigureLogger((logger, logContext, context) =>
// {
// var httpContext = context.HttpContext;//获取httpContext
// //获取客户端信息
// var client = App.GetService<IAppService>().UserAgent;
// // 获取控制器/操作描述器
// var controllerActionDescriptor = context.ActionDescriptor as ControllerActionDescriptor;
// //操作名称默认是控制器名加方法名,自定义操作名称要在action上加Description特性
// var option = $"{controllerActionDescriptor.ControllerName}/{controllerActionDescriptor.ActionName}";
// var desc = App.CreateLocalizerByType(controllerActionDescriptor.ControllerTypeInfo.AsType())[controllerActionDescriptor.MethodInfo.Name];
// //获取特性
// option = desc.Value;//则将操作名称赋值为控制器上写的title
// logContext.Set(LoggingConst.CateGory, option);//传操作名称
// logContext.Set(LoggingConst.Operation, option);//传操作名称
// logContext.Set(LoggingConst.Client, client);//客户端信息
// logContext.Set(LoggingConst.Path, httpContext.Request.Path.Value);//请求地址
// logContext.Set(LoggingConst.Method, httpContext.Request.Method);//请求方法
// });
//});
//日志写入数据库配置
services.AddDatabaseLogging<DatabaseLoggingWriter>(options =>
{
options.WriteFilter = (logMsg) =>
{
return logMsg.LogName == "System.Logging.RequestAudit";
};
});
#endregion api日志
//已添加AddOptions
// 增加多语言支持配置信息
@@ -303,7 +205,7 @@ public class Startup : AppStartup
var certificate = new X509Certificate2("ThingsGateway.pfx", "ThingsGateway", X509KeyStorageFlags.EphemeralKeySet);
#endif
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo("keys"))
.PersistKeysToFileSystem(new DirectoryInfo("Keys"))
.ProtectKeysWithCertificate(certificate)
.UseCryptographicAlgorithms(new AuthenticatedEncryptorConfiguration
{
@@ -371,7 +273,7 @@ public class Startup : AppStartup
// 特定文件类型(文件后缀)处理
var contentTypeProvider = GetFileExtensionContentTypeProvider();
var contentTypeProvider = FS.GetFileExtensionContentTypeProvider();
// contentTypeProvider.Mappings[".文件后缀"] = "MIME 类型";
app.UseStaticFiles(new StaticFileOptions
{
@@ -415,32 +317,4 @@ public class Startup : AppStartup
}
/// <summary>
/// 初始化文件 ContentType 提供器
/// </summary>
/// <returns></returns>
private static FileExtensionContentTypeProvider GetFileExtensionContentTypeProvider()
{
var fileExtensionProvider = new FileExtensionContentTypeProvider();
fileExtensionProvider.Mappings[".iec"] = "application/octet-stream";
fileExtensionProvider.Mappings[".patch"] = "application/octet-stream";
fileExtensionProvider.Mappings[".apk"] = "application/vnd.android.package-archive";
fileExtensionProvider.Mappings[".pem"] = "application/x-x509-user-cert";
fileExtensionProvider.Mappings[".gzip"] = "application/x-gzip";
fileExtensionProvider.Mappings[".7zip"] = "application/zip";
fileExtensionProvider.Mappings[".jpg2"] = "image/jp2";
fileExtensionProvider.Mappings[".et"] = "application/kset";
fileExtensionProvider.Mappings[".dps"] = "application/ksdps";
fileExtensionProvider.Mappings[".cdr"] = "application/x-coreldraw";
fileExtensionProvider.Mappings[".shtml"] = "text/html";
fileExtensionProvider.Mappings[".php"] = "application/x-httpd-php";
fileExtensionProvider.Mappings[".php3"] = "application/x-httpd-php";
fileExtensionProvider.Mappings[".php4"] = "application/x-httpd-php";
fileExtensionProvider.Mappings[".phtml"] = "application/x-httpd-php";
fileExtensionProvider.Mappings[".pcd"] = "image/x-photo-cd";
fileExtensionProvider.Mappings[".bcmap"] = "application/octet-stream";
fileExtensionProvider.Mappings[".properties"] = "application/octet-stream";
fileExtensionProvider.Mappings[".m3u8"] = "application/x-mpegURL";
return fileExtensionProvider;
}
}

View File

@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="$(SolutionDir)Version.props" />
<Import Project="..\..\Version.props" />
<PropertyGroup>
@@ -16,6 +16,7 @@
<!--动态适用GC-->
<GarbageCollectionAdaptationMode>1</GarbageCollectionAdaptationMode>
<CETCompat>false</CETCompat>
<!--使用自托管线程池-->
<!--<UseWindowsThreadPool>false</UseWindowsThreadPool> -->
@@ -44,9 +45,7 @@
</Content>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" Version="$(NET9Version)" />
</ItemGroup>
<!--安装服务守护-->
<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0' ">

View File

@@ -41,19 +41,19 @@ public abstract class PrimaryKeyEntity : PrimaryIdEntity
[SugarColumn(ColumnDescription = "扩展信息", ColumnDataType = StaticConfig.CodeFirst_BigString, IsNullable = true)]
[IgnoreExcel]
[AutoGenerateColumn(Ignore = true)]
public virtual string? ExtJson { get; set; }
public virtual string ExtJson { get; set; }
}
public interface IBaseEntity
{
DateTime? CreateTime { get; set; }
string? CreateUser { get; set; }
DateTime CreateTime { get; set; }
string CreateUser { get; set; }
long CreateUserId { get; set; }
bool IsDelete { get; set; }
int? SortCode { get; set; }
DateTime? UpdateTime { get; set; }
string? UpdateUser { get; set; }
long? UpdateUserId { get; set; }
int SortCode { get; set; }
DateTime UpdateTime { get; set; }
string UpdateUser { get; set; }
long UpdateUserId { get; set; }
}
/// <summary>
@@ -61,13 +61,22 @@ public interface IBaseEntity
/// </summary>
public abstract class BaseEntity : PrimaryKeyEntity, IBaseEntity
{
private long createUserId;
private long updateUserId;
private DateTime createTime;
private DateTime updateTime;
private int sortCode;
private bool isDelete = false;
private string createUser;
private string updateUser;
/// <summary>
/// 创建时间
/// </summary>
[SugarColumn(ColumnDescription = "创建时间", IsOnlyIgnoreUpdate = true, IsNullable = true)]
[IgnoreExcel]
[AutoGenerateColumn(Visible = false, IsVisibleWhenAdd = false, IsVisibleWhenEdit = false)]
public virtual DateTime? CreateTime { get; set; }
public virtual DateTime CreateTime { get => createTime; set => createTime = value; }
/// <summary>
/// 创建人
@@ -76,7 +85,7 @@ public abstract class BaseEntity : PrimaryKeyEntity, IBaseEntity
[IgnoreExcel]
[NotNull]
[AutoGenerateColumn(Ignore = true)]
public virtual string? CreateUser { get; set; }
public virtual string CreateUser { get => createUser; set => createUser = value; }
/// <summary>
/// 创建者Id
@@ -84,7 +93,7 @@ public abstract class BaseEntity : PrimaryKeyEntity, IBaseEntity
[SugarColumn(ColumnDescription = "创建者Id", IsOnlyIgnoreUpdate = true, IsNullable = true)]
[IgnoreExcel]
[AutoGenerateColumn(Ignore = true)]
public virtual long CreateUserId { get; set; }
public virtual long CreateUserId { get => createUserId; set => createUserId = value; }
/// <summary>
/// 软删除
@@ -92,8 +101,7 @@ public abstract class BaseEntity : PrimaryKeyEntity, IBaseEntity
[SugarColumn(ColumnDescription = "软删除", IsNullable = true)]
[IgnoreExcel]
[AutoGenerateColumn(Ignore = true)]
public virtual bool IsDelete { get; set; } = false;
public virtual bool IsDelete { get => isDelete; set => isDelete = value; }
/// <summary>
/// 更新时间
@@ -101,7 +109,7 @@ public abstract class BaseEntity : PrimaryKeyEntity, IBaseEntity
[SugarColumn(ColumnDescription = "更新时间", IsOnlyIgnoreInsert = true, IsNullable = true)]
[IgnoreExcel]
[AutoGenerateColumn(Visible = false, IsVisibleWhenAdd = false, IsVisibleWhenEdit = false)]
public virtual DateTime? UpdateTime { get; set; }
public virtual DateTime UpdateTime { get => updateTime; set => updateTime = value; }
/// <summary>
/// 更新人
@@ -109,7 +117,7 @@ public abstract class BaseEntity : PrimaryKeyEntity, IBaseEntity
[SugarColumn(ColumnDescription = "更新人", IsOnlyIgnoreInsert = true, IsNullable = true)]
[IgnoreExcel]
[AutoGenerateColumn(Ignore = true)]
public virtual string? UpdateUser { get; set; }
public virtual string UpdateUser { get => updateUser; set => updateUser = value; }
/// <summary>
/// 修改者Id
@@ -117,7 +125,7 @@ public abstract class BaseEntity : PrimaryKeyEntity, IBaseEntity
[SugarColumn(ColumnDescription = "修改者Id", IsOnlyIgnoreInsert = true, IsNullable = true)]
[IgnoreExcel]
[AutoGenerateColumn(Ignore = true)]
public virtual long? UpdateUserId { get; set; }
public virtual long UpdateUserId { get => updateUserId; set => updateUserId = value; }
/// <summary>
/// 排序码
@@ -125,7 +133,7 @@ public abstract class BaseEntity : PrimaryKeyEntity, IBaseEntity
[SugarColumn(ColumnDescription = "排序码", IsNullable = true)]
[AutoGenerateColumn(Visible = false, DefaultSort = true, Sortable = true, DefaultSortOrder = SortOrder.Asc)]
[IgnoreExcel]
public virtual int? SortCode { get; set; }
public virtual int SortCode { get => sortCode; set => sortCode = value; }
}
public interface IBaseDataEntity

View File

@@ -11,8 +11,6 @@
using BootstrapBlazor.Components;
using Mapster;
using MiniExcelLibs;
using MiniExcelLibs.Attributes;
using MiniExcelLibs.OpenXml;
@@ -45,7 +43,7 @@ public static class ExportExcelExtensions
#region
var type = typeof(T);
var propertyInfos = type.GetRuntimeProperties().Where(a => a.GetCustomAttribute<IgnoreExcelAttribute>() == null)
var propertyInfos = type.GetRuntimeProperties().Where(a => a.GetCustomAttribute<IgnoreExcelAttribute>(false) == null)
.OrderBy(
a =>
{
@@ -104,7 +102,7 @@ public static class ExportExcelExtensions
int index = 0;
foreach (var item in data)
{
var ignore = item.GetCustomAttribute<IgnoreExcelAttribute>() != null;
var ignore = item.GetCustomAttribute<IgnoreExcelAttribute>(false) != null;
//描述
var desc = type.GetPropertyDisplayName(item.Name);
//数据源增加

View File

@@ -124,16 +124,8 @@ public class SugarAopService : ISugarAopService
//执行时间超过1秒
if (db.Ado.SqlExecutionTime.TotalSeconds > 1)
{
//代码CS文件名
var fileName = db.Ado.SqlStackTrace.FirstFileName;
//代码行数
var fileLine = db.Ado.SqlStackTrace.FirstLine;
//方法名
var FirstMethodName = db.Ado.SqlStackTrace.FirstMethodName;
DbContext.WriteLog($"{fileName}-{FirstMethodName}-{fileLine} 执行时间超过1秒");
DbContext.WriteLog($"SQL执行时间超过1秒");
DbContext.WriteLogWithSql(UtilMethods.GetNativeSql(sql, pars));
}
};
}

View File

@@ -161,7 +161,7 @@ public class BaseService<T> : IDataService<T>, IDisposable where T : class, new(
/// <returns></returns>
protected SqlSugarClient GetDB()
{
return DbContext.Db.GetConnectionScopeWithAttr<T>().CopyNew();
return DbContext.GetDB<T>();
}

View File

@@ -55,7 +55,7 @@ public static class CodeFirstUtils
var entityType = seedType.GetInterfaces().First().GetGenericArguments().First();//获取实体类型
var tenantAtt = entityType.GetCustomAttribute<TenantAttribute>();//获取sqlSugar多库特性
if (tenantAtt == null) continue;//如果没有多库特性就下一个
using var db = DbContext.Db.GetConnectionScope(tenantAtt.configId.ToString()).CopyNew();//获取数据库对象
using var db = DbContext.GetDB(tenantAtt.configId.ToString());//获取数据库对象
var config = DbContext.DbConfigs.FirstOrDefault(u => u.ConfigId.ToString() == tenantAtt.configId.ToString());//获取数据库配置
if (config?.InitSeedData != true) continue;
var entityInfo = db.EntityMaintenance.GetEntityInfo(entityType);
@@ -66,7 +66,8 @@ public static class CodeFirstUtils
{
// 按主键进行批量增加和更新
var storage = db.StorageableByObject(seedData.ToList()).ToStorage();
if (ignoreAdd == null) storage.AsInsertable.ExecuteCommand();//执行插入
if (ignoreAdd == null)
storage.AsInsertable.ExecuteCommand();//执行插入
if (ignoreUpdate == null && config.IsUpdateSeedData) storage.AsUpdateable.ExecuteCommand();//只有没有忽略更新的特性才执行更新
}
else// 没有主键或者不是预定义的主键(有重复的可能)
@@ -98,7 +99,7 @@ public static class CodeFirstUtils
var ignoreInit = entityType.GetCustomAttribute<IgnoreInitTableAttribute>();//获取忽略初始化特性
if (ignoreInit != null) continue;//如果有忽略初始化特性
if (tenantAtt == null) continue;//如果没有多库特性就下一个
using var db = DbContext.Db.GetConnectionScope(tenantAtt.configId.ToString()).CopyNew();//获取数据库对象
using var db = DbContext.GetDB(tenantAtt.configId.ToString());//获取数据库对象
var splitTable = entityType.GetCustomAttribute<SplitTableAttribute>();//获取自动分表特性
if (splitTable == null)//如果特性是空
db.CodeFirst.InitTables(entityType);//普通创建

View File

@@ -24,7 +24,7 @@ public static class DbContext
/// <summary>
/// SqlSugar 数据库实例
/// </summary>
public static readonly SqlSugarScope Db;
private static readonly SqlSugarClient Db;
/// <summary>
/// 读取配置文件中的 ConnectionStrings:Sqlsugar 配置节点
@@ -37,9 +37,28 @@ public static class DbContext
/// <returns></returns>
public static SqlSugarClient GetDB<T>()
{
return Db.GetConnectionScopeWithAttr<T>().CopyNew();
return Db.GetConnectionWithAttr<T>().CopyNew();
}
/// <summary>
/// 获取数据库连接
/// </summary>
/// <returns></returns>
public static SqlSugarClient GetDB()
{
return Db;
}
/// <summary>
/// 获取数据库连接
/// </summary>
/// <returns></returns>
public static SqlSugarClient GetDB(string tenant)
{
return Db.GetConnection(tenant).CopyNew();//获取数据库对象
}
private static ISugarAopService sugarAopService;
private static ISugarAopService SugarAopService
{
@@ -62,7 +81,7 @@ public static class DbContext
{
DbConfigs.ForEach(it =>
{
var sqlsugarScope = db.GetConnectionScope(it.ConfigId);//获取当前库
var sqlsugarScope = db.GetConnection(it.ConfigId);//获取当前库
MoreSetting(sqlsugarScope);//更多设置
SugarAopService.AopSetting(sqlsugarScope, it.IsShowSql);//aop配置
}
@@ -75,7 +94,7 @@ public static class DbContext
/// 实体更多配置
/// </summary>
/// <param name="db"></param>
private static void MoreSetting(SqlSugarScopeProvider db)
private static void MoreSetting(SqlSugarProvider db)
{
db.CurrentConnectionConfig.MoreSettings = new ConnMoreSettings
{

View File

@@ -24,7 +24,7 @@ namespace ThingsGateway.Admin.Application;
/// 种子数据工具类
/// </summary>
[ThingsGateway.DependencyInjection.SuppressSniffer]
public static class SeedDataUtil
public static partial class SeedDataUtil
{
/// <summary>
/// 获取List列表
@@ -53,9 +53,7 @@ public static class SeedDataUtil
if (!string.IsNullOrEmpty(json))//如果有内容
{
//字段没有数据的替换成null
json = Regex.Replace(json, "\\\"[^\"]+?\\\": \\\"\\\"", match => match.Value.Replace("\"\"", "null"));
json = SeedDataRegex().Replace(json, match => match.Value.Replace("\"\"", "null"));
var jtoken = JToken.Parse(json);
jtoken = jtoken.SelectToken("Records") ?? jtoken.SelectToken("RECORDS");
@@ -96,6 +94,9 @@ public static class SeedDataUtil
return seedData;
}
[GeneratedRegex("\\\"[^\"]+?\\\": \\\"\\\"")]
private static partial Regex SeedDataRegex();
}
/// <summary>

View File

@@ -10,7 +10,6 @@
using BootstrapBlazor.Components;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using ThingsGateway.SqlSugar;
@@ -32,11 +31,10 @@ public class Startup : AppStartup
StaticConfig.EnableAllWhereIF = true;
services.AddSingleton<ISugarAopService, SugarAopService>();
}
public void Use(IApplicationBuilder applicationBuilder)
public void Use(IServiceProvider serviceProvider)
{

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="$(SolutionDir)Version.props" />
<Import Project="$(SolutionDir)PackNuget.props" />
<Import Project="..\..\Version.props" />
<Import Project="..\..\PackNuget.props" />
<PropertyGroup>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
@@ -11,17 +11,18 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BootstrapBlazor.TableExport" Version="9.2.5" />
<PackageReference Include="BootstrapBlazor.TableExport" Version="9.2.6" />
</ItemGroup>
<ItemGroup>
<None Include="..\README.md" Pack="true" PackagePath="\" />
<None Include="..\README.zh-CN.md" Pack="true" PackagePath="\" />
<None Remove="$(SolutionDir)..\README.md" Pack="false" PackagePath="\" />
<None Remove="$(SolutionDir)..\README.zh-CN.md" Pack="false" PackagePath="\" />
<None Remove="..\..\..\README.md" Pack="false" PackagePath="\" />
<None Remove="..\..\..\README.zh-CN.md" Pack="false" PackagePath="\" />
</ItemGroup>
<ItemGroup>
<!--<PackageReference Include="ThingsGateway.Razor" Version="$(SourceGeneratorVersion)" />-->
<ProjectReference Include="..\ThingsGateway.Razor\ThingsGateway.Razor.csproj" />
<ProjectReference Include="..\ThingsGateway.SqlSugar\ThingsGateway.SqlSugar.csproj" />
<!--<PackageReference Include="SqlSugarCore" Version="5.1.4.195" />-->

View File

@@ -0,0 +1 @@
https://gitee.com/dotnetchina/Furion/commit/ef5310cbf2618584c1bfc812253309299ec620d7

View File

@@ -15,6 +15,8 @@ using System.Reflection;
using System.Text;
using ThingsGateway;
using ThingsGateway.NewLife.Caching;
using ThingsGateway.NewLife.Redis.Extensions;
using ThingsGateway.UnifyResult;
namespace Microsoft.Extensions.DependencyInjection;
@@ -198,11 +200,44 @@ public static class AppServiceCollectionExtensions
{
// 注册全局配置选项
services.AddConfigurableOptions<AppSettingsOptions>();
services.AddConfigurableOptions<CacheOptions>();
// 注册内存和分布式内存
services.AddMemoryCache();
services.AddDistributedMemoryCache();
var cacheOptions = App.GetConfig<CacheOptions>("Cache", true);
// 缓存
if (cacheOptions.CacheType == CacheType.Memory)
{
services.AddSingleton<ICache, MemoryCache>(a => new()
{
Capacity = cacheOptions.MemoryCacheOptions.Capacity,
Expire = cacheOptions.MemoryCacheOptions.Expire,
Period = cacheOptions.MemoryCacheOptions.Period
});
}
else if (cacheOptions.CacheType == CacheType.Redis)
{
services.AddDistributedRedisCache(options =>
{
options.Db = cacheOptions.RedisCacheOptions.Db;
options.Configuration = cacheOptions.RedisCacheOptions.Configuration;
options.UserName = cacheOptions.RedisCacheOptions.UserName;
options.Password = cacheOptions.RedisCacheOptions.Password;
options.Server = cacheOptions.RedisCacheOptions.Server;
options.Timeout = cacheOptions.RedisCacheOptions.Timeout;
options.Prefix = cacheOptions.RedisCacheOptions.Prefix;
options.InstanceName = cacheOptions.RedisCacheOptions.InstanceName;
options.Expire = cacheOptions.RedisCacheOptions.Expire;
});
}
// 注册全局依赖注入
services.AddDependencyInjection();
@@ -210,7 +245,7 @@ public static class AppServiceCollectionExtensions
services.AddStartups();
// 添加对象映射
services.AddObjectMapper();
//services.AddObjectMapper();
// 默认内置 GBKWindows-1252, Shift-JIS, GB2312 编码支持
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

View File

@@ -133,7 +133,7 @@ internal static class InternalApp
}
/// <summary>
/// 配置 Furion 框架(非 Web
/// 配置框架(非 Web
/// </summary>
/// <param name="hostApplicationBuilder"></param>
/// <param name="autoRegisterBackgroundService"></param>
@@ -151,6 +151,8 @@ internal static class InternalApp
// 存储服务提供器
InternalServices = hostApplicationBuilder.Services;
// 存储根服务
hostApplicationBuilder.Services.AddHostedService<GenericHostLifetimeEventsHostedService>();

View File

@@ -9,7 +9,6 @@
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
using System.Security.Cryptography;
using System.Text;
@@ -47,27 +46,24 @@ public static class AESEncryption
if (iv != null && iv.Length != 16) throw new ArgumentException("The IV length must be 16 bytes.");
}
using var encryptor = aesAlg.CreateEncryptor();
using var msEncrypt = new MemoryStream();
using (var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
using (var swEncrypt = new StreamWriter(csEncrypt, Encoding.UTF8))
byte[] cipherBytes;
using (var encryptor = aesAlg.CreateEncryptor())
{
swEncrypt.Write(text);
var plainBytes = Encoding.UTF8.GetBytes(text);
cipherBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
}
var encryptedContent = msEncrypt.ToArray();
// 仅在未提供 IV 时拼接 IV
if (mode != CipherMode.ECB && iv == null)
{
var result = new byte[aesAlg.IV.Length + encryptedContent.Length];
var result = new byte[aesAlg.IV.Length + cipherBytes.Length];
Buffer.BlockCopy(aesAlg.IV, 0, result, 0, aesAlg.IV.Length);
Buffer.BlockCopy(encryptedContent, 0, result, aesAlg.IV.Length, encryptedContent.Length);
Buffer.BlockCopy(cipherBytes, 0, result, aesAlg.IV.Length, cipherBytes.Length);
return Convert.ToBase64String(result);
}
// 如果是 ECB 模式,直接返回密文的 Base64 编码
return Convert.ToBase64String(encryptedContent);
return Convert.ToBase64String(cipherBytes);
}
/// <summary>
@@ -112,11 +108,26 @@ public static class AESEncryption
}
using var decryptor = aesAlg.CreateDecryptor();
using var msDecrypt = new MemoryStream(fullCipher);
using var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read);
using var srDecrypt = new StreamReader(csDecrypt, Encoding.UTF8);
var plainBytes = decryptor.TransformFinalBlock(fullCipher, 0, fullCipher.Length);
return srDecrypt.ReadToEnd();
// 手动移除 PKCS7 填充
int padCount = plainBytes[^1];
if (padCount > 0 && padCount <= 16)
{
var validPadding = true;
for (var i = 1; i <= padCount; i++)
{
if (plainBytes[^i] != padCount)
{
validPadding = false;
break;
}
}
if (validPadding)
Array.Resize(ref plainBytes, plainBytes.Length - padCount);
}
return Encoding.UTF8.GetString(plainBytes);
}
/// <summary>
@@ -131,7 +142,6 @@ public static class AESEncryption
/// <returns>加密后的字节数组</returns>
public static byte[] Encrypt(byte[] bytes, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7, bool isBase64 = false)
{
// 验证密钥长度
var bKey = !isBase64 ? Encoding.UTF8.GetBytes(skey) : Convert.FromBase64String(skey);
if (bKey.Length != 16 && bKey.Length != 24 && bKey.Length != 32) throw new ArgumentException("The key length must be 16, 24, or 32 bytes.");
@@ -142,29 +152,23 @@ public static class AESEncryption
if (mode != CipherMode.ECB)
{
aesAlg.IV = iv ?? GenerateRandomIV();
aesAlg.IV = iv ?? (mode == CipherMode.CBC ? GenerateRandomIV() : throw new ArgumentException("IV is required for CBC mode."));
if (aesAlg.IV.Length != 16) throw new ArgumentException("The IV length must be 16 bytes.");
}
using var memoryStream = new MemoryStream();
using (var cryptoStream = new CryptoStream(memoryStream, aesAlg.CreateEncryptor(), CryptoStreamMode.Write))
byte[] cipherBytes;
using (var encryptor = aesAlg.CreateEncryptor())
{
cryptoStream.Write(bytes, 0, bytes.Length);
cryptoStream.FlushFinalBlock();
cipherBytes = encryptor.TransformFinalBlock(bytes, 0, bytes.Length);
}
var encryptedContent = memoryStream.ToArray();
if (mode == CipherMode.ECB)
return cipherBytes;
// 仅在未提供 IV 时拼接 IV
if (mode != CipherMode.ECB && iv == null)
{
var result = new byte[aesAlg.IV.Length + encryptedContent.Length];
Buffer.BlockCopy(aesAlg.IV, 0, result, 0, aesAlg.IV.Length);
Buffer.BlockCopy(encryptedContent, 0, result, aesAlg.IV.Length, encryptedContent.Length);
return result;
}
return encryptedContent;
var result = new byte[aesAlg.IV.Length + cipherBytes.Length];
Buffer.BlockCopy(aesAlg.IV, 0, result, 0, aesAlg.IV.Length);
Buffer.BlockCopy(cipherBytes, 0, result, aesAlg.IV.Length, cipherBytes.Length);
return result;
}
/// <summary>
@@ -179,7 +183,6 @@ public static class AESEncryption
/// <returns></returns>
public static byte[] Decrypt(byte[] bytes, string skey, byte[] iv = null, CipherMode mode = CipherMode.CBC, PaddingMode padding = PaddingMode.PKCS7, bool isBase64 = false)
{
// 验证密钥长度
var bKey = !isBase64 ? Encoding.UTF8.GetBytes(skey) : Convert.FromBase64String(skey);
if (bKey.Length != 16 && bKey.Length != 24 && bKey.Length != 32) throw new ArgumentException("The key length must be 16, 24, or 32 bytes.");
@@ -188,28 +191,48 @@ public static class AESEncryption
aesAlg.Mode = mode;
aesAlg.Padding = padding;
byte[] cipherBytes;
if (mode != CipherMode.ECB)
{
if (iv == null)
{
// 提取IV
if (bytes.Length < 16) throw new ArgumentException("The ciphertext length is insufficient to extract the IV.");
iv = bytes.Take(16).ToArray();
bytes = bytes.Skip(16).ToArray();
iv = [.. bytes.Take(16)];
cipherBytes = [.. bytes.Skip(16)];
}
else
{
if (iv.Length != 16) throw new ArgumentException("The IV length must be 16 bytes.");
cipherBytes = bytes;
}
aesAlg.IV = iv;
}
else
{
cipherBytes = bytes;
}
using var memoryStream = new MemoryStream(bytes);
using var cryptoStream = new CryptoStream(memoryStream, aesAlg.CreateDecryptor(), CryptoStreamMode.Read);
using var originalStream = new MemoryStream();
using var decryptor = aesAlg.CreateDecryptor();
var plainBytes = decryptor.TransformFinalBlock(cipherBytes, 0, cipherBytes.Length);
cryptoStream.CopyTo(originalStream);
return originalStream.ToArray();
// 手动移除 PKCS7 填充
int padCount = plainBytes[^1];
if (padCount > 0 && padCount <= 16)
{
var validPadding = true;
for (var i = 1; i <= padCount; i++)
{
if (plainBytes[^i] != padCount)
{
validPadding = false;
break;
}
}
if (validPadding)
Array.Resize(ref plainBytes, plainBytes.Length - padCount);
}
return plainBytes;
}
/// <summary>

View File

@@ -67,13 +67,13 @@ internal static class Penetrates
["patch"] = "PATCH"
};
IsApiControllerCached = new ConcurrentDictionary<Type, bool>();
//IsApiControllerCached = new ConcurrentDictionary<Type, bool>();
}
/// <summary>
/// <see cref="IsApiController(Type)"/> 缓存集合
/// </summary>
private static readonly ConcurrentDictionary<Type, bool> IsApiControllerCached;
///// <summary>
///// <see cref="IsApiController(Type)"/> 缓存集合
///// </summary>
//private static readonly ConcurrentDictionary<Type, bool> IsApiControllerCached;
/// <summary>
/// 是否是Api控制器
@@ -82,13 +82,13 @@ internal static class Penetrates
/// <returns></returns>
internal static bool IsApiController(Type type)
{
return IsApiControllerCached.GetOrAdd(type, Function);
//return IsApiControllerCached.GetOrAdd(type, Function);
return Function(type);
// 本地静态方法
static bool Function(Type type)
{
// 排除 OData 控制器
if (type.Assembly.GetName().Name.StartsWith("Microsoft.AspNetCore.OData")) return false;
//// 排除 OData 控制器
//if (type.Assembly.GetName().Name.StartsWith("Microsoft.AspNetCore.OData")) return false;
// 不能是非公开、基元类型、值类型、抽象类、接口、泛型类
if (!type.IsPublic || type.IsPrimitive || type.IsValueType || type.IsAbstract || type.IsInterface || type.IsGenericType) return false;

View File

@@ -12,6 +12,8 @@
using System.Reflection;
using System.Text.Json;
using ThingsGateway.Shapeless;
namespace ThingsGateway.EventBus;
/// <summary>
@@ -80,6 +82,10 @@ public abstract class EventHandlerContext
{
return JsonSerializer.Deserialize<T>(jsonElement.GetRawText(), JsonSerializerOptions);
}
else if (typeof(T) == typeof(Clay))
{
return (T)(object)Clay.Parse(Source.Payload);
}
else
{
return (T)rawPayload;

View File

@@ -203,7 +203,10 @@ internal sealed class EventBusHostedService : BackgroundService
{
// 从事件存储器中读取一条
var eventSource = await _eventSourceStorer.ReadAsync(stoppingToken).ConfigureAwait(false);
if (eventSource is null)
{
return;
}
// 处理动态新增/删除事件订阅器
if (eventSource is EventSubscribeOperateSource subscribeOperateSource)
{

View File

@@ -68,8 +68,15 @@ internal sealed partial class ChannelEventSourceStorer : IEventSourceStorer
/// <returns>事件源对象</returns>
public async ValueTask<IEventSource> ReadAsync(CancellationToken cancellationToken)
{
// 读取一条事件源
var eventSource = await _channel.Reader.ReadAsync(cancellationToken).ConfigureAwait(false);
return eventSource;
try
{
// 读取一条事件源
return await _channel.Reader.ReadAsync(cancellationToken).ConfigureAwait(false);
}
// 正常取消,服务停止时触发
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
return null;
}
}
}

View File

@@ -37,15 +37,15 @@ public sealed class Retry
{
if (action == null) throw new ArgumentNullException(nameof(action));
InvokeAsync(async () =>
InvokeAsync(() =>
{
action();
await Task.CompletedTask.ConfigureAwait(false);
return Task.CompletedTask;
}, numRetries, retryTimeout, finalThrow, exceptionTypes, fallbackPolicy == null ? null
: async (ex) =>
: (ex) =>
{
fallbackPolicy?.Invoke(ex);
await Task.CompletedTask.ConfigureAwait(false);
return Task.CompletedTask;
}, retryAction).GetAwaiter().GetResult();
}

View File

@@ -93,6 +93,10 @@ public sealed class DatabaseLogger : ILogger, IDisposable
{
// 判断日志级别是否有效
if (!IsEnabled(logLevel)) return;
if (_options.NameFilter?.Invoke(_logName) == false)
{
return;
}
// 检查日志格式化器
if (formatter == null) throw new ArgumentNullException(nameof(formatter));

View File

@@ -51,7 +51,10 @@ public sealed class DatabaseLoggerOptions
/// 是否使用 UTC 时间戳,默认 false
/// </summary>
public bool UseUtcTimestamp { get; set; }
/// <summary>
/// 名称筛选
/// </summary>
public Func<string, bool> NameFilter { get; set; }
/// <summary>
/// 日期格式化
/// </summary>

View File

@@ -15,6 +15,7 @@ using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using ThingsGateway.NewLife.Caching;
using ThingsGateway.NewLife.Log;
namespace ThingsGateway.Logging;
@@ -33,7 +34,7 @@ public sealed class DatabaseLoggerProvider : ILoggerProvider, ISupportExternalSc
/// <summary>
/// 日志消息队列(线程安全)
/// </summary>
private readonly BlockingCollection<LogMessage> _logMessageQueue = new(12000);
private readonly BlockingCollection<LogMessage> _logMessageQueue = new(20000);
/// <summary>
/// 日志作用域提供器
@@ -135,7 +136,10 @@ public sealed class DatabaseLoggerProvider : ILoggerProvider, ISupportExternalSc
{
try
{
_logMessageQueue.Add(logMsg);
if (!_logMessageQueue.TryAdd(logMsg, 5000))
{
XTrace.Log.Warn($"{nameof(DatabaseLoggerProvider)} queue add fail");
}
return;
}
catch (InvalidOperationException) { }
@@ -160,8 +164,8 @@ public sealed class DatabaseLoggerProvider : ILoggerProvider, ISupportExternalSc
_databaseLoggingWriter = _serviceScope.ServiceProvider.GetRequiredService(databaseLoggingWriterType) as IDatabaseLoggingWriter;
// 创建长时间运行的后台任务,并将日志消息队列中数据写入存储中
_processQueueTask = Task.Factory.StartNew(async state => await ((DatabaseLoggerProvider)state).ProcessQueueAsync().ConfigureAwait(false)
, this, TaskCreationOptions.LongRunning);
_processQueueTask = Task.Factory.StartNew(ProcessQueueAsync
, TaskCreationOptions.LongRunning);
}
/// <summary>

View File

@@ -14,6 +14,7 @@ using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
using ThingsGateway.NewLife.Caching;
using ThingsGateway.NewLife.Log;
namespace ThingsGateway.Logging;
@@ -32,7 +33,7 @@ public sealed class FileLoggerProvider : ILoggerProvider, ISupportExternalScope
/// <summary>
/// 日志消息队列(线程安全)
/// </summary>
private readonly BlockingCollection<LogMessage> _logMessageQueue = new(12000);
private readonly BlockingCollection<LogMessage> _logMessageQueue = new(20000);
/// <summary>
/// 日志作用域提供器
@@ -90,8 +91,7 @@ public sealed class FileLoggerProvider : ILoggerProvider, ISupportExternalScope
_fileLoggingWriter = new FileLoggingWriter(this);
// 创建长时间运行的后台任务,并将日志消息队列中数据写入文件中
_processQueueTask = Task.Factory.StartNew(async state => await ((FileLoggerProvider)state).ProcessQueueAsync().ConfigureAwait(false)
, this, TaskCreationOptions.LongRunning);
_processQueueTask = Task.Factory.StartNew(ProcessQueueAsync, TaskCreationOptions.LongRunning);
}
/// <summary>
@@ -171,8 +171,10 @@ public sealed class FileLoggerProvider : ILoggerProvider, ISupportExternalScope
{
try
{
_logMessageQueue.Add(logMsg);
return;
if (!_logMessageQueue.TryAdd(logMsg, 5000))
{
XTrace.Log.Warn($"{nameof(DatabaseLoggerProvider)} queue add fail");
}
}
catch (InvalidOperationException) { }
catch { }

View File

@@ -1,57 +1,59 @@
// ------------------------------------------------------------------------
// 版权信息
// 版权归百小僧及百签科技(广东)有限公司所有。
// 所有权利保留。
// 官方网站https://baiqian.com
//
// 许可证信息
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
//// ------------------------------------------------------------------------
//// 版权信息
//// 版权归百小僧及百签科技(广东)有限公司所有。
//// 所有权利保留。
//// 官方网站https://baiqian.com
////
//// 许可证信息
//// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
//// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
//// ------------------------------------------------------------------------
using Mapster;
//using Mapster;
using System.Reflection;
//using System.Reflection;
using ThingsGateway;
//using ThingsGateway;
namespace Microsoft.Extensions.DependencyInjection;
//namespace Microsoft.Extensions.DependencyInjection;
/// <summary>
/// 对象映射拓展类
/// </summary>
[SuppressSniffer]
public static class ObjectMapperServiceCollectionExtensions
{
///// <summary>
///// 对象映射拓展类
///// </summary>
//[SuppressSniffer]
//public static class ObjectMapperServiceCollectionExtensions
//{
/// <summary>
/// 添加对象映射
/// </summary>
/// <param name="services">服务集合</param>
/// <returns></returns>
public static IServiceCollection AddObjectMapper(this IServiceCollection services)
{
// 判断是否安装了 Mapster 程序集
return services.AddObjectMapper(App.Assemblies.ToArray());
}
// /// <summary>
// /// 添加对象映射
// /// </summary>
// /// <param name="services">服务集合</param>
// /// <returns></returns>
// public static IServiceCollection AddObjectMapper(this IServiceCollection services)
// {
// // 判断是否安装了 Mapster 程序集
// return services.AddObjectMapper(App.Assemblies.ToArray());
// }
/// <summary>
/// 添加对象映射
/// </summary>
/// <param name="services">服务集合</param>
/// <param name="assemblies">扫描的程序集</param>
/// <returns></returns>
public static IServiceCollection AddObjectMapper(this IServiceCollection services, params Assembly[] assemblies)
{
// 获取全局映射配置
var config = TypeAdapterConfig.GlobalSettings;
// /// <summary>
// /// 添加对象映射
// /// </summary>
// /// <param name="services">服务集合</param>
// /// <param name="assemblies">扫描的程序集</param>
// /// <returns></returns>
// public static IServiceCollection AddObjectMapper(this IServiceCollection services, params Assembly[] assemblies)
// {
// // 获取全局映射配置
// var config = TypeAdapterConfig.GlobalSettings;
// 扫描所有继承 IRegister 接口的对象映射配置
if (assemblies?.Length > 0) config.Scan(assemblies);
// //config.Compiler = exp => exp.CompileFast();
// 配置支持依赖注入
services.AddSingleton(config);
// // 扫描所有继承 IRegister 接口的对象映射配置
// if (assemblies?.Length > 0) config.Scan(assemblies);
return services;
}
}
// // 配置支持依赖注入
// services.AddSingleton(config);
// return services;
// }
//}

View File

@@ -0,0 +1,66 @@
// ------------------------------------------------------------------------
// 版权信息
// 版权归百小僧及百签科技(广东)有限公司所有。
// 所有权利保留。
// 官方网站https://baiqian.com
//
// 许可证信息
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
using Microsoft.Extensions.Configuration;
using ThingsGateway.ConfigurableOptions;
using ThingsGateway.NewLife.Caching;
namespace ThingsGateway;
public enum CacheType
{
/// <summary>
/// 内存缓存
/// </summary>
Memory,
/// <summary>
/// Redis 缓存
/// </summary>
Redis
}
/// <summary>
/// 应用全局配置
/// </summary>
public sealed class CacheOptions : IConfigurableOptions<CacheOptions>
{
public CacheType CacheType { get; set; }
public MemoryCacheOptions MemoryCacheOptions { get; set; } = new MemoryCacheOptions();
public RedisCacheOptions RedisCacheOptions { get; set; } = new RedisCacheOptions();
/// <summary>
/// 后期配置
/// </summary>
/// <param name="options"></param>
/// <param name="configuration"></param>
public void PostConfigure(CacheOptions options, IConfiguration configuration)
{
}
}
public class MemoryCacheOptions
{
/// <summary>默认过期时间。避免Set操作时没有设置过期时间默认3600秒</summary>
public Int32 Expire { get; set; } = 3600;
/// <summary>容量。容量超标时采用LRU机制删除默认100_000</summary>
public Int32 Capacity { get; set; } = 100_000;
/// <summary>定时清理时间默认60秒</summary>
public Int32 Period { get; set; } = 60;
}
public class RedisCacheOptions : RedisOptions
{
}

View File

@@ -0,0 +1,208 @@

#if NET6_0_OR_GREATER
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using ThingsGateway.NewLife.Caching;
using ThingsGateway.NewLife.Caching.Services;
using ThingsGateway.NewLife.Configuration;
using ThingsGateway.NewLife.Extension;
using ThingsGateway.NewLife.Log;
namespace Microsoft.Extensions.DependencyInjection;
/// <summary>
/// DependencyInjectionExtensions
/// </summary>
public static class DependencyInjectionExtensions
{
/// <summary>注入FullRedis应用内可使用FullRedis/Redis/ICache/ICacheProvider</summary>
/// <param name="services"></param>
/// <param name="redis"></param>
/// <returns></returns>
public static IServiceCollection AddRedis(this IServiceCollection services, FullRedis? redis = null)
{
//if (redis == null) throw new ArgumentNullException(nameof(redis));
if (redis == null) return services.AddRedisCacheProvider();
services.AddBasic();
services.TryAddSingleton<ICache>(redis);
services.AddSingleton<Redis>(redis);
services.AddSingleton(redis);
// 注册Redis缓存服务
services.TryAddSingleton<ICacheProvider>(p =>
{
var provider = new RedisCacheProvider(p);
if (provider.Cache is not Redis) provider.Cache = redis;
provider.RedisQueue ??= redis;
return provider;
});
return services;
}
/// <summary>
/// Adds services for FullRedis to the specified Microsoft.Extensions.DependencyInjection.IServiceCollection.
/// </summary>
/// <param name="services"></param>
/// <param name="config"></param>
/// <param name="tracer"></param>
/// <returns></returns>
public static FullRedis AddRedis(this IServiceCollection services, String config, ITracer tracer = null!)
{
if (String.IsNullOrEmpty(config)) throw new ArgumentNullException(nameof(config));
var redis = new FullRedis();
redis.Init(config);
redis.Tracer = tracer;
services.AddRedis(redis);
return redis;
}
/// <summary>
/// Adds services for FullRedis to the specified Microsoft.Extensions.DependencyInjection.IServiceCollection.
/// </summary>
/// <param name="services"></param>
/// <param name="name"></param>
/// <param name="config"></param>
/// <param name="timeout"></param>
/// <param name="tracer"></param>
/// <returns></returns>
public static FullRedis AddRedis(this IServiceCollection services, String name, String config, Int32 timeout = 0, ITracer tracer = null!)
{
if (String.IsNullOrEmpty(config)) throw new ArgumentNullException(nameof(config));
var redis = new FullRedis();
if (!name.IsNullOrEmpty()) redis.Name = name;
redis.Init(config);
if (timeout > 0) redis.Timeout = timeout;
redis.Tracer = tracer;
services.AddRedis(redis);
return redis;
}
/// <summary>
/// Adds services for FullRedis to the specified Microsoft.Extensions.DependencyInjection.IServiceCollection.
/// </summary>
/// <param name="services"></param>
/// <param name="server"></param>
/// <param name="psssword"></param>
/// <param name="db"></param>
/// <param name="timeout"></param>
/// <param name="tracer"></param>
/// <returns></returns>
public static FullRedis AddRedis(this IServiceCollection services, String server, String psssword, Int32 db, Int32 timeout = 0, ITracer tracer = null!)
{
if (String.IsNullOrEmpty(server)) throw new ArgumentNullException(nameof(server));
var redis = new FullRedis(server, psssword, db);
if (timeout > 0) redis.Timeout = timeout;
redis.Tracer = tracer;
services.AddRedis(redis);
return redis;
}
/// <summary>添加Redis缓存</summary>
/// <param name="services"></param>
/// <param name="setupAction"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
public static IServiceCollection AddRedis(this IServiceCollection services, Action<RedisOptions> setupAction)
{
if (services == null)
throw new ArgumentNullException(nameof(services));
if (setupAction == null)
throw new ArgumentNullException(nameof(setupAction));
services.AddBasic();
services.AddOptions();
services.Configure(setupAction);
//services.Add(ServiceDescriptor.Singleton<ICache, FullRedis>());
services.AddSingleton(sp => new FullRedis(sp, sp.GetRequiredService<IOptions<RedisOptions>>().Value));
services.TryAddSingleton<ICache>(p => p.GetRequiredService<FullRedis>());
services.TryAddSingleton<Redis>(p => p.GetRequiredService<FullRedis>());
// 注册Redis缓存服务
services.TryAddSingleton<ICacheProvider>(p =>
{
var redis = p.GetRequiredService<FullRedis>();
var provider = new RedisCacheProvider(p);
if (provider.Cache is not Redis) provider.Cache = redis;
provider.RedisQueue ??= redis;
return provider;
});
return services;
}
/// <summary>
/// 添加键前缀的PrefixedRedis缓存
/// </summary>
/// <param name="services"></param>
/// <param name="setupAction"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
[Obsolete("=>AddRedis")]
public static IServiceCollection AddPrefixedRedis(this IServiceCollection services, Action<RedisOptions> setupAction)
{
if (services == null)
throw new ArgumentNullException(nameof(services));
if (setupAction == null)
throw new ArgumentNullException(nameof(setupAction));
services.AddBasic();
services.AddOptions();
services.Configure(setupAction);
services.AddSingleton(sp => new FullRedis(sp, sp.GetRequiredService<IOptions<RedisOptions>>().Value));
return services;
}
/// <summary>添加Redis缓存提供者ICacheProvider。从配置读取RedisCache和RedisQueue</summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddRedisCacheProvider(this IServiceCollection services)
{
services.AddBasic();
services.AddSingleton<ICacheProvider, RedisCacheProvider>();
services.TryAddSingleton<ICache>(p => p.GetRequiredService<ICacheProvider>().Cache);
services.TryAddSingleton<Redis>(p =>
{
var redis = p.GetRequiredService<ICacheProvider>().Cache as Redis;
if (redis == null) throw new InvalidOperationException("未配置Redis可在配置文件或配置中心指定名为RedisCache的连接字符串");
return redis;
});
services.TryAddSingleton<FullRedis>(p =>
{
var redis = p.GetRequiredService<ICacheProvider>().Cache as FullRedis;
if (redis == null) throw new InvalidOperationException("未配置Redis可在配置文件或配置中心指定名为RedisCache的连接字符串");
return redis;
});
return services;
}
static void AddBasic(this IServiceCollection services)
{
// 注册依赖项
services.TryAddSingleton<ILog>(XTrace.Log);
services.TryAddSingleton<ITracer>(DefaultTracer.Instance ??= new DefaultTracer());
if (!services.Any(e => e.ServiceType == typeof(IConfigProvider)))
services.TryAddSingleton<IConfigProvider>(JsonConfigProvider.LoadAppSettings());
}
}
#endif

View File

@@ -0,0 +1,116 @@
#if NET6_0_OR_GREATER
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Options;
using ThingsGateway.NewLife.Caching;
namespace ThingsGateway.NewLife.Redis.Extensions;
/// <summary>
/// Redis分布式缓存
/// </summary>
public class RedisCache : FullRedis, IDistributedCache, IDisposable
{
#region
/// <summary>刷新时的过期时间。默认24小时</summary>
public new TimeSpan Expire { get; set; } = TimeSpan.FromHours(24);
#endregion
#region
/// <summary>
/// 实例化Redis分布式缓存
/// </summary>
/// <param name="serviceProvider"></param>
/// <param name="optionsAccessor"></param>
/// <exception cref="ArgumentNullException"></exception>
public RedisCache(IServiceProvider serviceProvider, IOptions<RedisOptions> optionsAccessor) : base(serviceProvider, optionsAccessor.Value)
{
if (optionsAccessor == null) throw new ArgumentNullException(nameof(optionsAccessor));
}
#endregion
/// <summary>
/// 获取
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public Byte[]? Get(String key) => base.Get<Byte[]?>(key);
/// <summary>
/// 异步获取
/// </summary>
/// <param name="key"></param>
/// <param name="token"></param>
/// <returns></returns>
public Task<Byte[]?> GetAsync(String key, CancellationToken token = default) => Task.Run(() => base.Get<Byte[]>(key), token);
/// <summary>
/// 设置
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
/// <param name="options"></param>
/// <exception cref="ArgumentNullException"></exception>
public void Set(String key, Byte[] value, DistributedCacheEntryOptions options)
{
if (key == null) throw new ArgumentNullException(nameof(key));
if (value == null) throw new ArgumentNullException(nameof(value));
if (options == null)
base.Set(key, value);
else
if (options.AbsoluteExpiration != null)
base.Set(key, value, options.AbsoluteExpiration.Value - DateTime.Now);
else if (options.AbsoluteExpirationRelativeToNow != null)
base.Set(key, value, options.AbsoluteExpirationRelativeToNow.Value);
else if (options.SlidingExpiration != null)
base.Set(key, value, options.SlidingExpiration.Value);
else
base.Set(key, value);
}
/// <summary>
/// 异步设置
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
/// <param name="options"></param>
/// <param name="token"></param>
/// <returns></returns>
public Task SetAsync(String key, Byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default) => Task.Run(() => Set(key, value, options), token);
/// <summary>
/// 刷新
/// </summary>
/// <param name="key"></param>
/// <exception cref="ArgumentNullException"></exception>
public void Refresh(String key) => base.SetExpire(key, Expire);
/// <summary>
/// 异步刷新
/// </summary>
/// <param name="key"></param>
/// <param name="token"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
public Task RefreshAsync(String key, CancellationToken token = default) => Task.Run(() => Refresh(key), token);
/// <summary>
/// 删除
/// </summary>
/// <param name="key"></param>
public new void Remove(String key) => base.Remove(key);
/// <summary>
/// 异步删除
/// </summary>
/// <param name="key"></param>
/// <param name="token"></param>
/// <returns></returns>
public Task RemoveAsync(String key, CancellationToken token = default) => Task.Run(() => base.Remove(key), token);
}
#endif

View File

@@ -0,0 +1,63 @@
#if NET6_0_OR_GREATER
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;
using ThingsGateway.NewLife.Caching;
using ThingsGateway.NewLife.Caching.Services;
namespace ThingsGateway.NewLife.Redis.Extensions;
/// <summary>
/// Redis分布式缓存扩展
/// </summary>
public static class RedisCacheServiceCollectionExtensions
{
/// <summary>
/// 添加Redis分布式缓存应用内可使用RedisCache/FullRedis/Redis/IDistributedCache/ICache/ICacheProvider
/// </summary>
/// <param name="services"></param>
/// <param name="setupAction"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
public static IServiceCollection AddDistributedRedisCache(this IServiceCollection services, Action<RedisOptions> setupAction)
{
if (services == null)
throw new ArgumentNullException(nameof(services));
if (setupAction == null)
throw new ArgumentNullException(nameof(setupAction));
services.AddOptions();
services.Configure(setupAction);
services.AddSingleton(sp => new RedisCache(sp, sp.GetRequiredService<IOptions<RedisOptions>>()));
services.AddSingleton<IDistributedCache>(sp => sp.GetRequiredService<RedisCache>());
services.TryAddSingleton<FullRedis>(sp => sp.GetRequiredService<RedisCache>());
services.TryAddSingleton<ICache>(p =>
{
var result = p.GetRequiredService<RedisCache>();
Cache.Default = result;
return result;
});
services.TryAddSingleton<ThingsGateway.NewLife.Caching.Redis>(p => p.GetRequiredService<RedisCache>());
// 注册Redis缓存服务
services.TryAddSingleton(p =>
{
var redis = p.GetRequiredService<RedisCache>();
var provider = new RedisCacheProvider(p);
if (provider.Cache is not ThingsGateway.NewLife.Caching.Redis) provider.Cache = redis;
provider.RedisQueue ??= redis;
return provider;
});
return services;
}
}
#endif

View File

@@ -0,0 +1,72 @@
#if NET6_0_OR_GREATER
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.Extensions.DependencyInjection;
namespace ThingsGateway.NewLife.Redis.Extensions;
/// <summary>Redis数据保护扩展</summary>
public static class RedisDataProtectionBuilderExtensions
{
private const String DataProtectionKeysName = "DataProtection-Keys";
///// <summary>存储数据保护Key到Redis自动识别已注入到容器的FullRedis或Redis单例</summary>
///// <param name="builder"></param>
///// <returns></returns>
///// <exception cref="ArgumentNullException"></exception>
//public static IDataProtectionBuilder PersistKeysToRedis(this IDataProtectionBuilder builder)
//{
// if (builder == null) throw new ArgumentNullException(nameof(builder));
// var redis = builder.Services.LastOrDefault(e => e.ServiceType == typeof(FullRedis))?.ImplementationInstance as NewLife.Caching.Redis;
// redis ??= builder.Services.LastOrDefault(e => e.ServiceType == typeof(NewLife.Caching.Redis))?.ImplementationInstance as NewLife.Caching.Redis;
// if (redis == null) throw new ArgumentNullException(nameof(redis));
// return PersistKeysToRedisInternal(builder, redis, DataProtectionKeysName);
//}
/// <summary>存储数据保护Key到Redis</summary>
/// <param name="builder"></param>
/// <param name="redis"></param>
/// <param name="key"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
public static IDataProtectionBuilder PersistKeysToRedis(this IDataProtectionBuilder builder, ThingsGateway.NewLife.Caching.Redis redis, String key = DataProtectionKeysName)
{
if (builder == null) throw new ArgumentNullException(nameof(builder));
if (redis == null) throw new ArgumentNullException(nameof(redis));
return PersistKeysToRedisInternal(builder, redis, key);
}
private static IDataProtectionBuilder PersistKeysToRedisInternal(IDataProtectionBuilder builder, ThingsGateway.NewLife.Caching.Redis redis, String key)
{
builder.Services.Configure(delegate (KeyManagementOptions options)
{
options.XmlRepository = new RedisXmlRepository(redis, key);
});
return builder;
}
/// <summary>存储数据保护Key到Redis</summary>
/// <param name="builder"></param>
/// <param name="redisFactory"></param>
/// <param name="key"></param>
/// <returns></returns>
/// <exception cref="ArgumentNullException"></exception>
public static IDataProtectionBuilder PersistKeysToRedis(this IDataProtectionBuilder builder, Func<ThingsGateway.NewLife.Caching.Redis> redisFactory, String key = DataProtectionKeysName)
{
if (builder == null) throw new ArgumentNullException(nameof(builder));
if (redisFactory == null) throw new ArgumentNullException(nameof(redisFactory));
builder.Services.Configure(delegate (KeyManagementOptions options)
{
options.XmlRepository = new RedisXmlRepository(redisFactory, key);
});
return builder;
}
}
#endif

View File

@@ -0,0 +1,69 @@
#if NET6_0_OR_GREATER
using Microsoft.AspNetCore.DataProtection.Repositories;
using System.Xml.Linq;
using ThingsGateway.NewLife.Log;
namespace ThingsGateway.NewLife.Redis.Extensions;
/// <summary>在Redis中存储Xml</summary>
public class RedisXmlRepository : IXmlRepository
{
private readonly ThingsGateway.NewLife.Caching.Redis? _redis;
private readonly Func<ThingsGateway.NewLife.Caching.Redis>? _redisFactory;
private readonly String _key;
/// <summary>实例化</summary>
/// <param name="redis"></param>
/// <param name="key"></param>
public RedisXmlRepository(ThingsGateway.NewLife.Caching.Redis redis, String key)
{
_redis = redis;
_key = key;
XTrace.WriteLine("DataProtection使用Redis持久化密钥Key={0}", key);
}
/// <summary>实例化</summary>
/// <param name="redisFactory"></param>
/// <param name="key"></param>
public RedisXmlRepository(Func<ThingsGateway.NewLife.Caching.Redis> redisFactory, String key)
{
_redisFactory = redisFactory;
_key = key;
XTrace.WriteLine("DataProtection使用Redis持久化密钥Key={0}", key);
}
/// <summary>获取所有元素</summary>
/// <returns></returns>
public IReadOnlyCollection<XElement> GetAllElements() => GetAllElementsCore().ToList().AsReadOnly();
/// <summary>遍历元素</summary>
/// <returns></returns>
private IEnumerable<XElement> GetAllElementsCore()
{
var rds = _redis ?? _redisFactory!();
var list = rds.GetList<String>(_key) ?? [];
foreach (var item in list)
{
yield return XElement.Parse(item);
}
}
/// <summary>存储元素</summary>
/// <param name="element"></param>
/// <param name="friendlyName"></param>
public void StoreElement(XElement element, String friendlyName)
{
var rds = _redis ?? _redisFactory!();
var list = rds.GetList<String>(_key);
list.Add(element.ToString(SaveOptions.DisableFormatting));
}
}
#endif

View File

@@ -280,7 +280,25 @@ public sealed class JobBuilder : JobDetail
return this;
}
/// <summary>
/// 设置为临时作业
/// </summary>
/// <returns><see cref="JobBuilder"/></returns>
public JobBuilder SetTemporary()
{
return SetTemporary(true);
}
/// <summary>
/// 设置是否是临时作业
/// </summary>
/// <param name="isTemporary"><see cref="bool"/></param>
/// <returns><see cref="JobBuilder"/></returns>
public JobBuilder SetTemporary(bool isTemporary)
{
Temporary = isTemporary;
return this;
}
/// <summary>
/// 设置作业信息额外数据
/// </summary>
@@ -364,6 +382,14 @@ public sealed class JobBuilder : JobDetail
{
return base.ClearProperties() as JobBuilder;
}
/// <summary>
/// 检查是否是临时作业
/// </summary>
/// <returns><see cref="bool"/></returns>
public bool CheckIsTemporary()
{
return Temporary;
}
/// <summary>
/// 构建 <see cref="JobDetail"/> 对象

View File

@@ -208,6 +208,21 @@ public sealed class ScheduleOptionsBuilder
return AddJob(SchedulerBuilder.Create<TJob>(triggerBuilders));
}
/// <summary>
/// 添加作业
/// </summary>
/// <typeparam name="TJob"><see cref="IJob"/> 实现类型</typeparam>
/// <param name="buildJob">作业构建器委托</param>
/// <param name="triggerBuilders">作业触发器构建器集合</param>
/// <returns><see cref="ScheduleOptionsBuilder"/></returns>
public ScheduleOptionsBuilder AddJob<TJob>(Action<JobBuilder> buildJob, params TriggerBuilder[] triggerBuilders)
where TJob : class, IJob
{
var jobBuilder = JobBuilder.Create<TJob>();
buildJob?.Invoke(jobBuilder);
return AddJob(jobBuilder, triggerBuilders);
}
/// <summary>
/// 添加作业
/// </summary>

View File

@@ -82,9 +82,54 @@ public abstract class JobExecutionContext
/// <summary>
/// 触发模式
/// </summary>
/// <remarks>默认为定时触发</remarks>
/// <remarks>默认为定时触发0:定时1手动</remarks>
public int Mode { get; internal set; }
/// <summary>
/// 存储作业执行过程中需要传递的数据
/// </summary>
public IDictionary<string, object> Items { get; internal set; }
/// <summary>
/// 获取作业执行过程中传递的数据
/// </summary>
/// <param name="key">键</param>
/// <returns><see cref="object"/></returns>
public object GetItem(string key)
{
return Items[key];
}
/// <summary>
/// 获取作业执行过程中传递的数据
/// </summary>
/// <typeparam name="T">目标类型</typeparam>
/// <param name="key">键</param>
/// <returns><typeparamref name="T"/></returns>
public T GetItem<T>(string key)
{
return Items.TryGetValue(key, out var value) ? (T)value : default;
}
/// <summary>
/// 获取作业执行过程中传递的数据
/// </summary>
/// <typeparam name="T">目标类型</typeparam>
/// <returns><see cref="IEnumerable{T}"/></returns>
public IEnumerable<T> GetItems<T>()
{
return Items.Values.OfType<T>();
}
/// <summary>
/// 获取作业执行过程中传递的数据
/// </summary>
/// <typeparam name="T">目标类型</typeparam>
/// <returns><typeparamref name="T"/></returns>
public T GetItem<T>()
{
return GetItems<T>().FirstOrDefault();
}
/// <summary>
/// 转换成 JSON 字符串
/// </summary>

View File

@@ -41,6 +41,6 @@ public sealed class JobFactoryContext
/// <summary>
/// 触发模式
/// </summary>
/// <remarks>默认为定时触发</remarks>
/// <remarks>默认为定时触发0:定时1手动</remarks>
public int Mode { get; internal set; }
}

View File

@@ -61,7 +61,7 @@ public sealed class PersistenceExecutionRecordContext
/// <summary>
/// 触发模式
/// </summary>
/// <remarks>默认为定时触发</remarks>
/// <remarks>默认为定时触发0:定时1手动</remarks>
public int Mode { get; }
/// <summary>

View File

@@ -45,7 +45,7 @@ public sealed class PersistenceTriggerContext : PersistenceContext
/// <summary>
/// 触发模式
/// </summary>
/// <remarks>默认为定时触发</remarks>
/// <remarks>默认为定时触发0:定时1手动</remarks>
public int Mode { get; internal set; }
/// <summary>

View File

@@ -15,6 +15,7 @@ using Microsoft.AspNetCore.Http;
using System.Collections.Concurrent;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace ThingsGateway.Schedule;
@@ -106,7 +107,11 @@ public sealed class ScheduleUIMiddleware
.Replace("%(DisplayEmptyTriggerJobs)", Options.DisplayEmptyTriggerJobs ? "true" : "false")
.Replace("%(DisplayHead)", Options.DisplayHead ? "true" : "false")
.Replace("%(DefaultExpandAllJobs)", Options.DefaultExpandAllJobs ? "true" : "false")
.Replace("%(UseUtcTimestamp)", ScheduleOptionsBuilder.UseUtcTimestampProperty ? "true" : "false");
.Replace("%(UseUtcTimestamp)", ScheduleOptionsBuilder.UseUtcTimestampProperty ? "true" : "false")
.Replace("%(Title)", Options.Title ?? string.Empty)
.Replace("%(Login.SessionKey)", Options.LoginConfig?.SessionKey ?? "schedule_session_key")
.Replace("%(Login.DefaultUsername)", Options.LoginConfig?.DefaultUsername ?? string.Empty)
.Replace("%(Login.DefaultPassword)", Options.LoginConfig?.DefaultPassword ?? string.Empty);
}
// 输出到客户端
@@ -114,7 +119,12 @@ public sealed class ScheduleUIMiddleware
await context.Response.WriteAsync(content).ConfigureAwait(false);
return;
}
// 处理刷新登录页面出现 404 情况
if (context.Request.Path.Equals(staticFilePath + "login", StringComparison.OrdinalIgnoreCase))
{
context.Response.Redirect(staticFilePath);
return;
}
// ================================ 处理 API 请求 ================================
// 如果不是以 API_REQUEST_PATH 开头,则跳过
@@ -136,19 +146,29 @@ public sealed class ScheduleUIMiddleware
// 允许跨域,设置返回 json
context.Response.ContentType = "application/json; charset=utf-8";
context.Response.Headers["Access-Control-Allow-Origin"] = "*";
context.Response.Headers["Access-Control-Allow-Headers"] = "*";
context.Response.Headers.AccessControlAllowOrigin = "*";
context.Response.Headers.AccessControlAllowHeaders = "*";
// 路由匹配
switch (action)
{
// 获取所有作业
case "/get-jobs":
var jobs = _schedulerFactory.GetJobsOfModels().OrderBy(u => u.JobDetail.GroupName);
var jobs = _schedulerFactory.GetJobsOfModels().OrderBy(u => u.JobDetail.GroupName).ThenBy(u => u.JobDetail.JobId);
// 输出 JSON
await context.Response.WriteAsync(SerializeToJson(jobs)).ConfigureAwait(false);
break;
// 获取所有运行记录
case "/timelines-log":
var allTimelines = _schedulerFactory.GetJobs()
.SelectMany(u => u.GetTriggers().SelectMany(s => s.GetTimelines()))
.OrderByDescending(u => u.CreatedTime)
.Take(20); // 默认取 20 条
// 输出 JSON
await context.Response.WriteAsync(SerializeToJson(allTimelines)).ConfigureAwait(false);
break;
// 操作作业
case "/operate-job":
// 获取作业 Id
@@ -270,7 +290,7 @@ public sealed class ScheduleUIMiddleware
// 推送更新
case "/check-change":
// 检查请求类型,是否为 text/event-stream 格式
if (!context.WebSockets.IsWebSocketRequest && context.Request.Headers["Accept"].ToString().Contains("text/event-stream"))
if (!context.WebSockets.IsWebSocketRequest && context.Request.Headers.Accept.ToString().Contains("text/event-stream"))
{
// 设置响应头的 content-type 为 text/event-stream
context.Response.ContentType = "text/event-stream";
@@ -311,6 +331,37 @@ public sealed class ScheduleUIMiddleware
_schedulerFactory.OnChanged -= Subscribe;
}
break;
// 登录验证
case "/login":
var username = context.Request.Form["username"];
var password = context.Request.Form["password"];
try
{
// 调用自定义验证逻辑
if (Options.LoginConfig?.OnLoging is not null && await Options.LoginConfig.OnLoging(username, password, context).ConfigureAwait(false))
{
context.Response.StatusCode = StatusCodes.Status200OK;
await context.Response.WriteAsync("OK").ConfigureAwait(false);
}
else
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
await context.Response.WriteAsync("username or password error").ConfigureAwait(false);
}
}
catch (Exception ex)
{
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
await context.Response.WriteAsync(ex.Message).ConfigureAwait(false);
}
break;
// 未处理接口
default:
context.Response.StatusCode = StatusCodes.Status404NotFound;
await context.Response.WriteAsync("Not Found").ConfigureAwait(false);
return;
}
}
@@ -324,7 +375,8 @@ public sealed class ScheduleUIMiddleware
// 初始化默认序列化选项
var jsonSerializerOptions = Penetrates.GetDefaultJsonSerializerOptions();
jsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
jsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
jsonSerializerOptions.WriteIndented = false;
return JsonSerializer.Serialize(obj, jsonSerializerOptions);
}
}

View File

@@ -9,6 +9,8 @@
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
using Microsoft.AspNetCore.Http;
namespace ThingsGateway.Schedule;
/// <summary>
@@ -28,6 +30,11 @@ public sealed class ScheduleUIOptions
/// </summary>
public bool EnableDirectoryBrowsing { get; set; } = false;
/// <summary>
/// 看板标题
/// </summary>
public string Title { get; set; } = "Schedule Dashboard";
/// <summary>
/// 生产环境关闭
/// </summary>
@@ -54,4 +61,38 @@ public sealed class ScheduleUIOptions
/// 是否默认展开所有作业
/// </summary>
public bool DefaultExpandAllJobs { get; set; } = false;
/// <summary>
/// 登录配置
/// </summary>
public LoginConfig LoginConfig = new();
}
/// <summary>
/// Schedule UI 登录配置
/// </summary>
public sealed class LoginConfig
{
/// <summary>
/// 客户端存储的 SessionKey
/// </summary>
public string SessionKey { get; set; } = "schedule_session_key";
/// <summary>
/// 默认登录名
/// </summary>
public string DefaultUsername { get; set; }
/// <summary>
/// 默认登录密码
/// </summary>
public string DefaultPassword { get; set; }
/// <summary>
/// 登录逻辑
/// </summary>
public Func<string, string, HttpContext, Task<bool>> OnLoging { get; set; } = (username, password, httpContext) =>
{
return Task.FromResult(username == "schedule" && string.IsNullOrWhiteSpace(password));
};
}

View File

@@ -1 +1 @@
window.apiconfig = { requestPath: "%(RequestPath)", hostAddress: "%(RequestPath)/api", options: { headers: { Accept: "application/json" }, cachePolicy: "no-cache" }, displayEmptyTriggerJobs: "%(DisplayEmptyTriggerJobs)", displayHead: "%(DisplayHead)", defaultExpandAllJobs: "%(DefaultExpandAllJobs)", useUtcTimestamp: "%(UseUtcTimestamp)" };
window.apiconfig = { requestPath: "%(RequestPath)", hostAddress: "%(RequestPath)/api", options: { headers: { Accept: "application/json" }, cachePolicy: "no-cache" }, displayEmptyTriggerJobs: "%(DisplayEmptyTriggerJobs)", displayHead: "%(DisplayHead)", defaultExpandAllJobs: "%(DefaultExpandAllJobs)", useUtcTimestamp: "%(UseUtcTimestamp)", title: "%(Title)", loginConfig: { sessionKey: "%(Login.SessionKey)", defaultUsername: "%(Login.DefaultUsername)", defaultPassword: "%(Login.DefaultPassword)" } };

View File

@@ -1,13 +1,13 @@
{
"files": {
"main.css": "/__schedule__/static/css/main.8eb42378.css",
"main.js": "/__schedule__/static/js/main.78b3d71a.js",
"main.css": "/__schedule__/static/css/main.fbe5db1c.css",
"main.js": "/__schedule__/static/js/main.851eb0b3.js",
"index.html": "/__schedule__/index.html",
"main.8eb42378.css.map": "/__schedule__/static/css/main.8eb42378.css.map",
"main.78b3d71a.js.map": "/__schedule__/static/js/main.78b3d71a.js.map"
"main.fbe5db1c.css.map": "/__schedule__/static/css/main.fbe5db1c.css.map",
"main.851eb0b3.js.map": "/__schedule__/static/js/main.851eb0b3.js.map"
},
"entrypoints": [
"static/css/main.8eb42378.css",
"static/js/main.78b3d71a.js"
"static/css/main.fbe5db1c.css",
"static/js/main.851eb0b3.js"
]
}

View File

@@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/__schedule__/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Schedule Dashboard"/><link rel="apple-touch-icon" href="/__schedule__/logo192.png"/><script defer="defer" src="/__schedule__/apiconfig.js"></script><title>Schedule Dashboard</title><script defer="defer" src="/__schedule__/static/js/main.78b3d71a.js"></script><link href="/__schedule__/static/css/main.8eb42378.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/__schedule__/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Schedule Dashboard"/><link rel="apple-touch-icon" href="/__schedule__/logo192.png"/><script src="/__schedule__/apiconfig.js"></script><title>Schedule Dashboard</title><script defer="defer" src="/__schedule__/static/js/main.851eb0b3.js"></script><link href="/__schedule__/static/css/main.fbe5db1c.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div><script>document.title=window.apiconfig.title</script></body></html>

Some files were not shown because too many files have changed in this diff Show More