Compare commits
42 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
1b2be585af | ||
![]() |
83736647e7 | ||
![]() |
b06405717d | ||
![]() |
298a1f2ed4 | ||
![]() |
74a47a1983 | ||
![]() |
a48a42abe4 | ||
![]() |
feb1d0a3c5 | ||
![]() |
92bca824e6 | ||
![]() |
025c699517 | ||
![]() |
53f8fbe4b1 | ||
![]() |
77bfabc41d | ||
![]() |
6427ee6ee0 | ||
![]() |
4c95997d62 | ||
![]() |
1d82cea40d | ||
![]() |
e78799424c | ||
![]() |
1000c8d38f | ||
![]() |
b2589fc634 | ||
![]() |
c80e57a4e8 | ||
![]() |
6510c3e289 | ||
![]() |
920e407d05 | ||
![]() |
7314c8901d | ||
![]() |
4e9f02b48c | ||
![]() |
9ae7602cb4 | ||
![]() |
aa8aa36aef | ||
![]() |
0174f7c6f2 | ||
![]() |
df9e7d6ff1 | ||
![]() |
b40ca920d3 | ||
![]() |
5a4b0a0e93 | ||
![]() |
a879edd68b | ||
![]() |
62e0a6ee9d | ||
![]() |
765e5564d4 | ||
![]() |
10eecac19b | ||
![]() |
59241b8faa | ||
![]() |
52b3097f04 | ||
![]() |
d922296b70 | ||
![]() |
aec91da28b | ||
![]() |
013ff394be | ||
![]() |
081e07473d | ||
![]() |
d33d900592 | ||
![]() |
29365c4ef9 | ||
![]() |
17a6189089 | ||
![]() |
003b8a3763 |
2
LICENSE
2
LICENSE
@@ -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
|
||||
|
@@ -0,0 +1,6 @@
|
||||
## Release 1.0
|
||||
|
||||
### New Rules
|
||||
Rule ID | Category | Severity | Notes
|
||||
--------|----------|----------|--------------------
|
||||
TG0001 | Conflict | Error | SetParametersAsyncGenerator
|
@@ -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>
|
@@ -0,0 +1,9 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
|
||||
// 此代码版权(除特别声明外的代码)归作者本人Diego所有
|
||||
// 源代码使用协议遵循本仓库的开源协议及附加协议
|
||||
// Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
|
||||
// Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
|
||||
// 使用文档:https://thingsgateway.cn/
|
||||
// QQ群:605534569
|
||||
//------------------------------------------------------------------------------
|
@@ -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);
|
||||
}
|
||||
|
||||
}
|
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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)
|
||||
|
@@ -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 登录错误次数
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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>
|
||||
|
@@ -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()}");
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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; } = "/";
|
||||
|
||||
}
|
@@ -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; }
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -8,5 +8,4 @@
|
||||
// QQ群:605534569
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
global using ThingsGateway;
|
||||
global using ThingsGateway.NewLife.Extension;
|
@@ -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);
|
||||
|
@@ -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); // 删除操作日志
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@@ -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
|
||||
{
|
@@ -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();
|
||||
}
|
||||
}
|
@@ -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}");
|
||||
}
|
||||
|
||||
|
@@ -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)}");
|
||||
}
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
namespace ThingsGateway.Admin.Application;
|
||||
|
||||
public class GithubOAuthSettings : GiteeOAuthSettings
|
||||
{
|
||||
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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"));
|
||||
|
@@ -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
|
||||
{
|
||||
|
@@ -10,6 +10,8 @@
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
using ThingsGateway.NewLife.DictionaryExtensions;
|
||||
|
||||
namespace ThingsGateway.Admin.Application;
|
||||
|
||||
/// <summary>
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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;//令牌数量
|
||||
|
@@ -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();//获取主管信息
|
||||
}
|
||||
|
||||
//获取按钮码
|
||||
|
@@ -11,9 +11,12 @@
|
||||
using BootstrapBlazor.Components;
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
using ThingsGateway.Extension;
|
||||
using ThingsGateway.UnifyResult;
|
||||
|
||||
namespace ThingsGateway.Admin.Application;
|
||||
@@ -64,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(IServiceProvider serviceProvider)
|
||||
{
|
||||
NewLife.Log.XTrace.UnhandledExceptionLogEnable = () => !App.HostApplicationLifetime.ApplicationStopping.IsCancellationRequested;
|
||||
|
||||
//检查ConfigId
|
||||
var configIdGroup = DbContext.DbConfigs.GroupBy(it => it.ConfigId);
|
||||
foreach (var configId in configIdGroup)
|
||||
@@ -78,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();//创建数据库,如果存在则不创建
|
||||
});
|
||||
|
@@ -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 "$(TargetDir)$(ProjectName).dll"" />
|
||||
<Exec WorkingDirectory="$(ProjectDir)" Command="dotnet mapster mapper -o MapsterGenerator -a "$(TargetDir)$(ProjectName).dll"" />
|
||||
</Target>-->
|
||||
|
||||
|
||||
</Project>
|
||||
|
@@ -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.
|
||||
|
@@ -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());
|
||||
}
|
||||
|
@@ -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>;
|
||||
}
|
||||
|
@@ -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>
|
||||
|
@@ -19,3 +19,4 @@ global using System.Diagnostics.CodeAnalysis;
|
||||
global using ThingsGateway.Razor;
|
||||
|
||||
[assembly: SuppressMessage("Reliability", "CA2007", Justification = "<挂起>", Scope = "module")]
|
||||
[assembly: GlobalGenerateSetParametersAsync(true)]
|
@@ -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();
|
||||
}
|
||||
|
||||
|
@@ -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();
|
||||
|
@@ -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);
|
||||
|
@@ -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>
|
||||
|
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -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; }
|
||||
|
@@ -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();
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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();
|
||||
}
|
||||
|
@@ -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>
|
||||
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
22
src/Admin/ThingsGateway.AdminServer/Configuration/Cache.json
Normal file
22
src/Admin/ThingsGateway.AdminServer/Configuration/Cache.json
Normal 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:"
|
||||
}
|
||||
}
|
||||
}
|
@@ -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
|
||||
{
|
||||
|
@@ -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;
|
||||
|
||||
@@ -60,8 +58,6 @@ public class Startup : AppStartup
|
||||
options.AddPersistence<JobPersistence>();
|
||||
});
|
||||
|
||||
// 缓存
|
||||
services.AddSingleton<ICache, MemoryCache>();
|
||||
|
||||
// 允许跨域
|
||||
services.AddCorsAccessor();
|
||||
@@ -152,101 +148,9 @@ public class Startup : AppStartup
|
||||
|
||||
|
||||
|
||||
#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
|
||||
// 增加多语言支持配置信息
|
||||
@@ -301,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
|
||||
{
|
||||
@@ -369,7 +273,7 @@ public class Startup : AppStartup
|
||||
|
||||
|
||||
// 特定文件类型(文件后缀)处理
|
||||
var contentTypeProvider = GetFileExtensionContentTypeProvider();
|
||||
var contentTypeProvider = FS.GetFileExtensionContentTypeProvider();
|
||||
// contentTypeProvider.Mappings[".文件后缀"] = "MIME 类型";
|
||||
app.UseStaticFiles(new StaticFileOptions
|
||||
{
|
||||
@@ -413,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;
|
||||
}
|
||||
}
|
||||
|
@@ -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' ">
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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);
|
||||
//数据源增加
|
||||
|
@@ -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));
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@@ -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>();
|
||||
}
|
||||
|
||||
|
||||
|
@@ -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);
|
||||
@@ -99,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);//普通创建
|
||||
|
@@ -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
|
||||
{
|
||||
|
@@ -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>
|
||||
|
@@ -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" />-->
|
||||
|
1
src/Admin/ThingsGateway.Furion/.txt
Normal file
1
src/Admin/ThingsGateway.Furion/.txt
Normal file
@@ -0,0 +1 @@
|
||||
https://gitee.com/dotnetchina/Furion/commit/ef5310cbf2618584c1bfc812253309299ec620d7
|
@@ -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();
|
||||
|
||||
// 默认内置 GBK,Windows-1252, Shift-JIS, GB2312 编码支持
|
||||
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
|
||||
|
@@ -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>();
|
||||
|
||||
|
@@ -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>
|
||||
|
@@ -87,8 +87,8 @@ internal static class Penetrates
|
||||
// 本地静态方法
|
||||
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;
|
||||
|
@@ -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;
|
||||
|
@@ -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)
|
||||
{
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
|
||||
|
@@ -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));
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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 { }
|
||||
|
@@ -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;
|
||||
// }
|
||||
//}
|
||||
|
66
src/Admin/ThingsGateway.Furion/Redis/Cache/CacheOptions.cs
Normal file
66
src/Admin/ThingsGateway.Furion/Redis/Cache/CacheOptions.cs
Normal 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
|
||||
{
|
||||
|
||||
}
|
@@ -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
|
116
src/Admin/ThingsGateway.Furion/Redis/Extension/RedisCache.cs
Normal file
116
src/Admin/ThingsGateway.Furion/Redis/Extension/RedisCache.cs
Normal 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
|
@@ -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
|
@@ -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
|
@@ -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
|
@@ -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"/> 对象
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -41,6 +41,6 @@ public sealed class JobFactoryContext
|
||||
/// <summary>
|
||||
/// 触发模式
|
||||
/// </summary>
|
||||
/// <remarks>默认为定时触发</remarks>
|
||||
/// <remarks>默认为定时触发,0:定时,1:手动</remarks>
|
||||
public int Mode { get; internal set; }
|
||||
}
|
@@ -61,7 +61,7 @@ public sealed class PersistenceExecutionRecordContext
|
||||
/// <summary>
|
||||
/// 触发模式
|
||||
/// </summary>
|
||||
/// <remarks>默认为定时触发</remarks>
|
||||
/// <remarks>默认为定时触发,0:定时,1:手动</remarks>
|
||||
public int Mode { get; }
|
||||
|
||||
/// <summary>
|
||||
|
@@ -45,7 +45,7 @@ public sealed class PersistenceTriggerContext : PersistenceContext
|
||||
/// <summary>
|
||||
/// 触发模式
|
||||
/// </summary>
|
||||
/// <remarks>默认为定时触发</remarks>
|
||||
/// <remarks>默认为定时触发,0:定时,1:手动</remarks>
|
||||
public int Mode { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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));
|
||||
};
|
||||
}
|
@@ -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)" } };
|
@@ -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"
|
||||
]
|
||||
}
|
@@ -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>
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user