diff --git a/Admin.NET/Admin.NET.Application/Admin.NET.Application.csproj b/Admin.NET/Admin.NET.Application/Admin.NET.Application.csproj
index 46fd9c10..1635185b 100644
--- a/Admin.NET/Admin.NET.Application/Admin.NET.Application.csproj
+++ b/Admin.NET/Admin.NET.Application/Admin.NET.Application.csproj
@@ -26,7 +26,9 @@
+
+
diff --git a/Admin.NET/Admin.NET.Web.Entry/App_Data/PluginCore.Config.json b/Admin.NET/Admin.NET.Web.Entry/App_Data/PluginCore.Config.json
new file mode 100644
index 00000000..cc4c80c2
--- /dev/null
+++ b/Admin.NET/Admin.NET.Web.Entry/App_Data/PluginCore.Config.json
@@ -0,0 +1 @@
+{"Admin":{"UserName":"admin","Password":"ABC12345"},"FrontendMode":"LocalEmbedded","RemoteFrontend":"https://cdn.jsdelivr.net/gh/yiyungent/plugincore-admin-frontend@0.3.1/dist-cdn","PluginWidgetDebug":false}
\ No newline at end of file
diff --git a/Admin.NET/Admin.NET.Web.Entry/App_Data/plugin.config.json b/Admin.NET/Admin.NET.Web.Entry/App_Data/plugin.config.json
new file mode 100644
index 00000000..fe43402b
--- /dev/null
+++ b/Admin.NET/Admin.NET.Web.Entry/App_Data/plugin.config.json
@@ -0,0 +1 @@
+{"EnabledPlugins":["Admin.NET.Plugin.Pay.Alipay"]}
\ No newline at end of file
diff --git a/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/Admin.NET.Plugin.Pay.Alipay.deps.json b/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/Admin.NET.Plugin.Pay.Alipay.deps.json
new file mode 100644
index 00000000..1fc448b2
--- /dev/null
+++ b/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/Admin.NET.Plugin.Pay.Alipay.deps.json
@@ -0,0 +1,738 @@
+{
+ "runtimeTarget": {
+ "name": ".NETCoreApp,Version=v8.0",
+ "signature": ""
+ },
+ "compilationOptions": {},
+ "targets": {
+ ".NETCoreApp,Version=v8.0": {
+ "Admin.NET.Plugin.Pay.Alipay/1.0.0": {
+ "dependencies": {
+ "Admin.NET.Plugin.Core": "1.0.0",
+ "Furion.Pure": "4.9.7.3"
+ },
+ "runtime": {
+ "Admin.NET.Plugin.Pay.Alipay.dll": {}
+ }
+ },
+ "Ben.Demystifier/0.4.1": {
+ "dependencies": {
+ "System.Reflection.Metadata": "7.0.0"
+ },
+ "runtime": {
+ "lib/netstandard2.1/Ben.Demystifier.dll": {
+ "assemblyVersion": "0.4.0.0",
+ "fileVersion": "0.4.0.2"
+ }
+ }
+ },
+ "Furion.Pure/4.9.7.3": {
+ "dependencies": {
+ "Furion.Pure.Extras.DependencyModel.CodeAnalysis": "4.9.7.3",
+ "MiniProfiler.AspNetCore.Mvc": "4.5.4",
+ "Swashbuckle.AspNetCore": "7.2.0"
+ },
+ "runtime": {
+ "lib/net8.0/Furion.Pure.dll": {
+ "assemblyVersion": "4.9.7.3",
+ "fileVersion": "4.9.7.3"
+ }
+ }
+ },
+ "Furion.Pure.Extras.DependencyModel.CodeAnalysis/4.9.7.3": {
+ "dependencies": {
+ "Ben.Demystifier": "0.4.1",
+ "Microsoft.AspNetCore.Mvc.NewtonsoftJson": "8.0.11",
+ "Microsoft.AspNetCore.Razor.Language": "6.0.36",
+ "Microsoft.CodeAnalysis.CSharp": "4.8.0",
+ "Microsoft.Extensions.DependencyModel": "8.0.2",
+ "System.Text.Json": "8.0.5",
+ "System.Text.RegularExpressions": "4.3.1"
+ },
+ "runtime": {
+ "lib/net8.0/Furion.Pure.Extras.DependencyModel.CodeAnalysis.dll": {
+ "assemblyVersion": "4.9.7.3",
+ "fileVersion": "4.9.7.3"
+ }
+ }
+ },
+ "Microsoft.AspNetCore.Http.Abstractions/2.3.0": {
+ "dependencies": {
+ "Microsoft.AspNetCore.Http.Features": "2.3.0",
+ "System.Text.Encodings.Web": "8.0.0"
+ }
+ },
+ "Microsoft.AspNetCore.Http.Features/2.3.0": {
+ "dependencies": {
+ "Microsoft.Extensions.Primitives": "8.0.0"
+ }
+ },
+ "Microsoft.AspNetCore.JsonPatch/8.0.11": {
+ "dependencies": {
+ "Microsoft.CSharp": "4.7.0",
+ "Newtonsoft.Json": "13.0.3"
+ },
+ "runtime": {
+ "lib/net8.0/Microsoft.AspNetCore.JsonPatch.dll": {
+ "assemblyVersion": "8.0.11.0",
+ "fileVersion": "8.0.1124.52116"
+ }
+ }
+ },
+ "Microsoft.AspNetCore.Mvc.NewtonsoftJson/8.0.11": {
+ "dependencies": {
+ "Microsoft.AspNetCore.JsonPatch": "8.0.11",
+ "Newtonsoft.Json": "13.0.3",
+ "Newtonsoft.Json.Bson": "1.0.2"
+ },
+ "runtime": {
+ "lib/net8.0/Microsoft.AspNetCore.Mvc.NewtonsoftJson.dll": {
+ "assemblyVersion": "8.0.11.0",
+ "fileVersion": "8.0.1124.52116"
+ }
+ }
+ },
+ "Microsoft.AspNetCore.Razor.Language/6.0.36": {
+ "runtime": {
+ "lib/netstandard2.0/Microsoft.AspNetCore.Razor.Language.dll": {
+ "assemblyVersion": "6.0.36.0",
+ "fileVersion": "6.0.3624.51604"
+ }
+ }
+ },
+ "Microsoft.CodeAnalysis.Analyzers/3.3.4": {},
+ "Microsoft.CodeAnalysis.Common/4.8.0": {
+ "dependencies": {
+ "Microsoft.CodeAnalysis.Analyzers": "3.3.4",
+ "System.Collections.Immutable": "7.0.0",
+ "System.Reflection.Metadata": "7.0.0",
+ "System.Runtime.CompilerServices.Unsafe": "6.0.0"
+ },
+ "runtime": {
+ "lib/net7.0/Microsoft.CodeAnalysis.dll": {
+ "assemblyVersion": "4.8.0.0",
+ "fileVersion": "4.800.23.55801"
+ }
+ },
+ "resources": {
+ "lib/net7.0/cs/Microsoft.CodeAnalysis.resources.dll": {
+ "locale": "cs"
+ },
+ "lib/net7.0/de/Microsoft.CodeAnalysis.resources.dll": {
+ "locale": "de"
+ },
+ "lib/net7.0/es/Microsoft.CodeAnalysis.resources.dll": {
+ "locale": "es"
+ },
+ "lib/net7.0/fr/Microsoft.CodeAnalysis.resources.dll": {
+ "locale": "fr"
+ },
+ "lib/net7.0/it/Microsoft.CodeAnalysis.resources.dll": {
+ "locale": "it"
+ },
+ "lib/net7.0/ja/Microsoft.CodeAnalysis.resources.dll": {
+ "locale": "ja"
+ },
+ "lib/net7.0/ko/Microsoft.CodeAnalysis.resources.dll": {
+ "locale": "ko"
+ },
+ "lib/net7.0/pl/Microsoft.CodeAnalysis.resources.dll": {
+ "locale": "pl"
+ },
+ "lib/net7.0/pt-BR/Microsoft.CodeAnalysis.resources.dll": {
+ "locale": "pt-BR"
+ },
+ "lib/net7.0/ru/Microsoft.CodeAnalysis.resources.dll": {
+ "locale": "ru"
+ },
+ "lib/net7.0/tr/Microsoft.CodeAnalysis.resources.dll": {
+ "locale": "tr"
+ },
+ "lib/net7.0/zh-Hans/Microsoft.CodeAnalysis.resources.dll": {
+ "locale": "zh-Hans"
+ },
+ "lib/net7.0/zh-Hant/Microsoft.CodeAnalysis.resources.dll": {
+ "locale": "zh-Hant"
+ }
+ }
+ },
+ "Microsoft.CodeAnalysis.CSharp/4.8.0": {
+ "dependencies": {
+ "Microsoft.CodeAnalysis.Common": "4.8.0"
+ },
+ "runtime": {
+ "lib/net7.0/Microsoft.CodeAnalysis.CSharp.dll": {
+ "assemblyVersion": "4.8.0.0",
+ "fileVersion": "4.800.23.55801"
+ }
+ },
+ "resources": {
+ "lib/net7.0/cs/Microsoft.CodeAnalysis.CSharp.resources.dll": {
+ "locale": "cs"
+ },
+ "lib/net7.0/de/Microsoft.CodeAnalysis.CSharp.resources.dll": {
+ "locale": "de"
+ },
+ "lib/net7.0/es/Microsoft.CodeAnalysis.CSharp.resources.dll": {
+ "locale": "es"
+ },
+ "lib/net7.0/fr/Microsoft.CodeAnalysis.CSharp.resources.dll": {
+ "locale": "fr"
+ },
+ "lib/net7.0/it/Microsoft.CodeAnalysis.CSharp.resources.dll": {
+ "locale": "it"
+ },
+ "lib/net7.0/ja/Microsoft.CodeAnalysis.CSharp.resources.dll": {
+ "locale": "ja"
+ },
+ "lib/net7.0/ko/Microsoft.CodeAnalysis.CSharp.resources.dll": {
+ "locale": "ko"
+ },
+ "lib/net7.0/pl/Microsoft.CodeAnalysis.CSharp.resources.dll": {
+ "locale": "pl"
+ },
+ "lib/net7.0/pt-BR/Microsoft.CodeAnalysis.CSharp.resources.dll": {
+ "locale": "pt-BR"
+ },
+ "lib/net7.0/ru/Microsoft.CodeAnalysis.CSharp.resources.dll": {
+ "locale": "ru"
+ },
+ "lib/net7.0/tr/Microsoft.CodeAnalysis.CSharp.resources.dll": {
+ "locale": "tr"
+ },
+ "lib/net7.0/zh-Hans/Microsoft.CodeAnalysis.CSharp.resources.dll": {
+ "locale": "zh-Hans"
+ },
+ "lib/net7.0/zh-Hant/Microsoft.CodeAnalysis.CSharp.resources.dll": {
+ "locale": "zh-Hant"
+ }
+ }
+ },
+ "Microsoft.CSharp/4.7.0": {},
+ "Microsoft.Extensions.ApiDescription.Server/6.0.5": {},
+ "Microsoft.Extensions.DependencyInjection.Abstractions/9.0.1": {
+ "runtime": {
+ "lib/net8.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll": {
+ "assemblyVersion": "9.0.0.0",
+ "fileVersion": "9.0.124.61010"
+ }
+ }
+ },
+ "Microsoft.Extensions.DependencyModel/8.0.2": {
+ "runtime": {
+ "lib/net8.0/Microsoft.Extensions.DependencyModel.dll": {
+ "assemblyVersion": "8.0.0.2",
+ "fileVersion": "8.0.1024.46610"
+ }
+ }
+ },
+ "Microsoft.Extensions.Logging.Abstractions/9.0.1": {
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
+ "System.Diagnostics.DiagnosticSource": "9.0.1"
+ },
+ "runtime": {
+ "lib/net8.0/Microsoft.Extensions.Logging.Abstractions.dll": {
+ "assemblyVersion": "9.0.0.0",
+ "fileVersion": "9.0.124.61010"
+ }
+ }
+ },
+ "Microsoft.Extensions.Primitives/8.0.0": {},
+ "Microsoft.NETCore.Platforms/1.1.1": {},
+ "Microsoft.NETCore.Targets/1.1.3": {},
+ "Microsoft.OpenApi/1.6.22": {
+ "runtime": {
+ "lib/netstandard2.0/Microsoft.OpenApi.dll": {
+ "assemblyVersion": "1.6.22.0",
+ "fileVersion": "1.6.22.0"
+ }
+ }
+ },
+ "MiniProfiler.AspNetCore/4.5.4": {
+ "dependencies": {
+ "MiniProfiler.Shared": "4.5.4"
+ },
+ "runtime": {
+ "lib/net8.0/MiniProfiler.AspNetCore.dll": {
+ "assemblyVersion": "4.0.0.0",
+ "fileVersion": "4.5.4.47516"
+ }
+ }
+ },
+ "MiniProfiler.AspNetCore.Mvc/4.5.4": {
+ "dependencies": {
+ "MiniProfiler.AspNetCore": "4.5.4"
+ },
+ "runtime": {
+ "lib/net8.0/MiniProfiler.AspNetCore.Mvc.dll": {
+ "assemblyVersion": "4.0.0.0",
+ "fileVersion": "4.5.4.47516"
+ }
+ }
+ },
+ "MiniProfiler.Shared/4.5.4": {
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1"
+ },
+ "runtime": {
+ "lib/net8.0/MiniProfiler.Shared.dll": {
+ "assemblyVersion": "4.0.0.0",
+ "fileVersion": "4.5.4.47516"
+ }
+ }
+ },
+ "Newtonsoft.Json/13.0.3": {
+ "runtime": {
+ "lib/net6.0/Newtonsoft.Json.dll": {
+ "assemblyVersion": "13.0.0.0",
+ "fileVersion": "13.0.3.27908"
+ }
+ }
+ },
+ "Newtonsoft.Json.Bson/1.0.2": {
+ "dependencies": {
+ "Newtonsoft.Json": "13.0.3"
+ },
+ "runtime": {
+ "lib/netstandard2.0/Newtonsoft.Json.Bson.dll": {
+ "assemblyVersion": "1.0.0.0",
+ "fileVersion": "1.0.2.22727"
+ }
+ }
+ },
+ "SharpZipLib/1.4.2": {
+ "runtime": {
+ "lib/net6.0/ICSharpCode.SharpZipLib.dll": {
+ "assemblyVersion": "1.4.2.13",
+ "fileVersion": "1.4.2.13"
+ }
+ }
+ },
+ "Swashbuckle.AspNetCore/7.2.0": {
+ "dependencies": {
+ "Microsoft.Extensions.ApiDescription.Server": "6.0.5",
+ "Swashbuckle.AspNetCore.Swagger": "7.2.0",
+ "Swashbuckle.AspNetCore.SwaggerGen": "7.2.0",
+ "Swashbuckle.AspNetCore.SwaggerUI": "7.2.0"
+ }
+ },
+ "Swashbuckle.AspNetCore.Swagger/7.2.0": {
+ "dependencies": {
+ "Microsoft.OpenApi": "1.6.22"
+ },
+ "runtime": {
+ "lib/net8.0/Swashbuckle.AspNetCore.Swagger.dll": {
+ "assemblyVersion": "7.2.0.0",
+ "fileVersion": "7.2.0.956"
+ }
+ }
+ },
+ "Swashbuckle.AspNetCore.SwaggerGen/7.2.0": {
+ "dependencies": {
+ "Swashbuckle.AspNetCore.Swagger": "7.2.0"
+ },
+ "runtime": {
+ "lib/net8.0/Swashbuckle.AspNetCore.SwaggerGen.dll": {
+ "assemblyVersion": "7.2.0.0",
+ "fileVersion": "7.2.0.956"
+ }
+ }
+ },
+ "Swashbuckle.AspNetCore.SwaggerUI/7.2.0": {
+ "runtime": {
+ "lib/net8.0/Swashbuckle.AspNetCore.SwaggerUI.dll": {
+ "assemblyVersion": "7.2.0.0",
+ "fileVersion": "7.2.0.956"
+ }
+ }
+ },
+ "System.Collections.Immutable/7.0.0": {},
+ "System.Diagnostics.DiagnosticSource/9.0.1": {
+ "runtime": {
+ "lib/net8.0/System.Diagnostics.DiagnosticSource.dll": {
+ "assemblyVersion": "9.0.0.0",
+ "fileVersion": "9.0.124.61010"
+ }
+ }
+ },
+ "System.Reflection.Metadata/7.0.0": {
+ "dependencies": {
+ "System.Collections.Immutable": "7.0.0"
+ }
+ },
+ "System.Runtime/4.3.1": {
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.1.1",
+ "Microsoft.NETCore.Targets": "1.1.3"
+ }
+ },
+ "System.Runtime.CompilerServices.Unsafe/6.0.0": {},
+ "System.Text.Encodings.Web/8.0.0": {},
+ "System.Text.Json/8.0.5": {},
+ "System.Text.RegularExpressions/4.3.1": {
+ "dependencies": {
+ "System.Runtime": "4.3.1"
+ }
+ },
+ "Admin.NET.Plugin.Core/1.0.0": {
+ "dependencies": {
+ "PluginCore.AspNetCore": "1.4.3"
+ },
+ "runtime": {
+ "Admin.NET.Plugin.Core.dll": {
+ "assemblyVersion": "1.0.0",
+ "fileVersion": "1.0.0.0"
+ }
+ }
+ },
+ "PluginCore/2.2.5": {
+ "dependencies": {
+ "Microsoft.Extensions.Logging.Abstractions": "9.0.1",
+ "PluginCore.IPlugins": "0.9.1",
+ "SharpZipLib": "1.4.2"
+ },
+ "runtime": {
+ "PluginCore.dll": {
+ "assemblyVersion": "2.2.5",
+ "fileVersion": "2.2.5.0"
+ }
+ }
+ },
+ "PluginCore.AspNetCore/1.4.3": {
+ "dependencies": {
+ "PluginCore": "2.2.5",
+ "PluginCore.IPlugins.AspNetCore": "0.1.1"
+ },
+ "runtime": {
+ "PluginCore.AspNetCore.dll": {
+ "assemblyVersion": "1.4.3",
+ "fileVersion": "1.4.3.0"
+ }
+ }
+ },
+ "PluginCore.IPlugins/0.9.1": {
+ "dependencies": {
+ "Microsoft.Extensions.DependencyInjection.Abstractions": "9.0.1",
+ "System.Text.Json": "8.0.5"
+ },
+ "runtime": {
+ "PluginCore.IPlugins.dll": {
+ "assemblyVersion": "0.9.1",
+ "fileVersion": "0.9.1.0"
+ }
+ }
+ },
+ "PluginCore.IPlugins.AspNetCore/0.1.1": {
+ "dependencies": {
+ "Microsoft.AspNetCore.Http.Abstractions": "2.3.0",
+ "PluginCore.IPlugins": "0.9.1"
+ },
+ "runtime": {
+ "PluginCore.IPlugins.AspNetCore.dll": {
+ "assemblyVersion": "0.1.1",
+ "fileVersion": "0.1.1.0"
+ }
+ }
+ }
+ }
+ },
+ "libraries": {
+ "Admin.NET.Plugin.Pay.Alipay/1.0.0": {
+ "type": "project",
+ "serviceable": false,
+ "sha512": ""
+ },
+ "Ben.Demystifier/0.4.1": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-axFeEMfmEORy3ipAzOXG/lE+KcNptRbei3F0C4kQCdeiQtW+qJW90K5iIovITGrdLt8AjhNCwk5qLSX9/rFpoA==",
+ "path": "ben.demystifier/0.4.1",
+ "hashPath": "ben.demystifier.0.4.1.nupkg.sha512"
+ },
+ "Furion.Pure/4.9.7.3": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-4HAtU6kg3/5cY6ImtbKs+qDa+8BteyD9DUIcCVSq3oo8AWE2ZeMJmXOD9rx+5/3mRQXkt5DIXVa4jIG32FjTHw==",
+ "path": "furion.pure/4.9.7.3",
+ "hashPath": "furion.pure.4.9.7.3.nupkg.sha512"
+ },
+ "Furion.Pure.Extras.DependencyModel.CodeAnalysis/4.9.7.3": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-v6pMgb+tkTeGFpmG98CY8Mv4QNBfTDiDsZmKGb6DEJxTPhdmr7l8onSdTlBuUlxfo94KXIOf7Gq+czr0RX/qbw==",
+ "path": "furion.pure.extras.dependencymodel.codeanalysis/4.9.7.3",
+ "hashPath": "furion.pure.extras.dependencymodel.codeanalysis.4.9.7.3.nupkg.sha512"
+ },
+ "Microsoft.AspNetCore.Http.Abstractions/2.3.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-39r9PPrjA6s0blyFv5qarckjNkaHRA5B+3b53ybuGGNTXEj1/DStQJ4NWjFL6QTRQpL9zt7nDyKxZdJOlcnq+Q==",
+ "path": "microsoft.aspnetcore.http.abstractions/2.3.0",
+ "hashPath": "microsoft.aspnetcore.http.abstractions.2.3.0.nupkg.sha512"
+ },
+ "Microsoft.AspNetCore.Http.Features/2.3.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-f10WUgcsKqrkmnz6gt8HeZ7kyKjYN30PO7cSic1lPtH7paPtnQqXPOveul/SIPI43PhRD4trttg4ywnrEmmJpA==",
+ "path": "microsoft.aspnetcore.http.features/2.3.0",
+ "hashPath": "microsoft.aspnetcore.http.features.2.3.0.nupkg.sha512"
+ },
+ "Microsoft.AspNetCore.JsonPatch/8.0.11": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-l1tFnQm2LtFE3M9YRM/bdwtxxCV50Y5jnN0LjliQH1sqvWsN46++Uu3QCJL9IdOweFvXSf3Shi7DI/Vc1jkdKA==",
+ "path": "microsoft.aspnetcore.jsonpatch/8.0.11",
+ "hashPath": "microsoft.aspnetcore.jsonpatch.8.0.11.nupkg.sha512"
+ },
+ "Microsoft.AspNetCore.Mvc.NewtonsoftJson/8.0.11": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-XcfFd8e0g2M0mcAKVNgoHJtWYJfKrPntHhgqiZ1Ci37i3AEJbM0GHIa715i0UPSksiKmDxsJWXnM3rg8keF/Zg==",
+ "path": "microsoft.aspnetcore.mvc.newtonsoftjson/8.0.11",
+ "hashPath": "microsoft.aspnetcore.mvc.newtonsoftjson.8.0.11.nupkg.sha512"
+ },
+ "Microsoft.AspNetCore.Razor.Language/6.0.36": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-n5Mg5D0aRrhHJJ6bJcwKqQydIFcgUq0jTlvuynoJjwA2IvAzh8Aqf9cpYagofQbIlIXILkCP6q6FgbngyVtpYA==",
+ "path": "microsoft.aspnetcore.razor.language/6.0.36",
+ "hashPath": "microsoft.aspnetcore.razor.language.6.0.36.nupkg.sha512"
+ },
+ "Microsoft.CodeAnalysis.Analyzers/3.3.4": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-AxkxcPR+rheX0SmvpLVIGLhOUXAKG56a64kV9VQZ4y9gR9ZmPXnqZvHJnmwLSwzrEP6junUF11vuc+aqo5r68g==",
+ "path": "microsoft.codeanalysis.analyzers/3.3.4",
+ "hashPath": "microsoft.codeanalysis.analyzers.3.3.4.nupkg.sha512"
+ },
+ "Microsoft.CodeAnalysis.Common/4.8.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-/jR+e/9aT+BApoQJABlVCKnnggGQbvGh7BKq2/wI1LamxC+LbzhcLj4Vj7gXCofl1n4E521YfF9w0WcASGg/KA==",
+ "path": "microsoft.codeanalysis.common/4.8.0",
+ "hashPath": "microsoft.codeanalysis.common.4.8.0.nupkg.sha512"
+ },
+ "Microsoft.CodeAnalysis.CSharp/4.8.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-+3+qfdb/aaGD8PZRCrsdobbzGs1m9u119SkkJt8e/mk3xLJz/udLtS2T6nY27OTXxBBw10HzAbC8Z9w08VyP/g==",
+ "path": "microsoft.codeanalysis.csharp/4.8.0",
+ "hashPath": "microsoft.codeanalysis.csharp.4.8.0.nupkg.sha512"
+ },
+ "Microsoft.CSharp/4.7.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-pTj+D3uJWyN3My70i2Hqo+OXixq3Os2D1nJ2x92FFo6sk8fYS1m1WLNTs0Dc1uPaViH0YvEEwvzddQ7y4rhXmA==",
+ "path": "microsoft.csharp/4.7.0",
+ "hashPath": "microsoft.csharp.4.7.0.nupkg.sha512"
+ },
+ "Microsoft.Extensions.ApiDescription.Server/6.0.5": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-Ckb5EDBUNJdFWyajfXzUIMRkhf52fHZOQuuZg/oiu8y7zDCVwD0iHhew6MnThjHmevanpxL3f5ci2TtHQEN6bw==",
+ "path": "microsoft.extensions.apidescription.server/6.0.5",
+ "hashPath": "microsoft.extensions.apidescription.server.6.0.5.nupkg.sha512"
+ },
+ "Microsoft.Extensions.DependencyInjection.Abstractions/9.0.1": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-Tr74eP0oQ3AyC24ch17N8PuEkrPbD0JqIfENCYqmgKYNOmL8wQKzLJu3ObxTUDrjnn4rHoR1qKa37/eQyHmCDA==",
+ "path": "microsoft.extensions.dependencyinjection.abstractions/9.0.1",
+ "hashPath": "microsoft.extensions.dependencyinjection.abstractions.9.0.1.nupkg.sha512"
+ },
+ "Microsoft.Extensions.DependencyModel/8.0.2": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-mUBDZZRgZrSyFOsJ2qJJ9fXfqd/kXJwf3AiDoqLD9m6TjY5OO/vLNOb9fb4juC0487eq4hcGN/M2Rh/CKS7QYw==",
+ "path": "microsoft.extensions.dependencymodel/8.0.2",
+ "hashPath": "microsoft.extensions.dependencymodel.8.0.2.nupkg.sha512"
+ },
+ "Microsoft.Extensions.Logging.Abstractions/9.0.1": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-w2gUqXN/jNIuvqYwX3lbXagsizVNXYyt6LlF57+tMve4JYCEgCMMAjRce6uKcDASJgpMbErRT1PfHy2OhbkqEA==",
+ "path": "microsoft.extensions.logging.abstractions/9.0.1",
+ "hashPath": "microsoft.extensions.logging.abstractions.9.0.1.nupkg.sha512"
+ },
+ "Microsoft.Extensions.Primitives/8.0.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-bXJEZrW9ny8vjMF1JV253WeLhpEVzFo1lyaZu1vQ4ZxWUlVvknZ/+ftFgVheLubb4eZPSwwxBeqS1JkCOjxd8g==",
+ "path": "microsoft.extensions.primitives/8.0.0",
+ "hashPath": "microsoft.extensions.primitives.8.0.0.nupkg.sha512"
+ },
+ "Microsoft.NETCore.Platforms/1.1.1": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-TMBuzAHpTenGbGgk0SMTwyEkyijY/Eae4ZGsFNYJvAr/LDn1ku3Etp3FPxChmDp5HHF3kzJuoaa08N0xjqAJfQ==",
+ "path": "microsoft.netcore.platforms/1.1.1",
+ "hashPath": "microsoft.netcore.platforms.1.1.1.nupkg.sha512"
+ },
+ "Microsoft.NETCore.Targets/1.1.3": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-3Wrmi0kJDzClwAC+iBdUBpEKmEle8FQNsCs77fkiOIw/9oYA07bL1EZNX0kQ2OMN3xpwvl0vAtOCYY3ndDNlhQ==",
+ "path": "microsoft.netcore.targets/1.1.3",
+ "hashPath": "microsoft.netcore.targets.1.1.3.nupkg.sha512"
+ },
+ "Microsoft.OpenApi/1.6.22": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-aBvunmrdu/x+4CaA/UP1Jx4xWGwk4kymhoIRnn2Vp+zi5/KOPQJ9EkSXHRUr01WcGKtYl3Au7XfkPJbU1G2sjQ==",
+ "path": "microsoft.openapi/1.6.22",
+ "hashPath": "microsoft.openapi.1.6.22.nupkg.sha512"
+ },
+ "MiniProfiler.AspNetCore/4.5.4": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-meedJsjpYOeHPhE8H6t+dGQ9zLxcCQVpi4DXzmxmYAXywmTzlo6jv2IASUv5QijTU0CxsROln3FHd8RsTO8Z8A==",
+ "path": "miniprofiler.aspnetcore/4.5.4",
+ "hashPath": "miniprofiler.aspnetcore.4.5.4.nupkg.sha512"
+ },
+ "MiniProfiler.AspNetCore.Mvc/4.5.4": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-+NqXyCy9aNdroPm6leW5+cpngtCnkCdoyOlJzvVN62uucSx+MYkx8jmKbgAt+aCP6aghADfHBExwrTIldHxapg==",
+ "path": "miniprofiler.aspnetcore.mvc/4.5.4",
+ "hashPath": "miniprofiler.aspnetcore.mvc.4.5.4.nupkg.sha512"
+ },
+ "MiniProfiler.Shared/4.5.4": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-f8ckFm/xTS8C2Bn4BdVc94dNvg+tRfk0e4XFaETOqRi6r0PUOyn3Z9jTQCVpB3R1pP5WiRsEIrqqxux95BVpTA==",
+ "path": "miniprofiler.shared/4.5.4",
+ "hashPath": "miniprofiler.shared.4.5.4.nupkg.sha512"
+ },
+ "Newtonsoft.Json/13.0.3": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==",
+ "path": "newtonsoft.json/13.0.3",
+ "hashPath": "newtonsoft.json.13.0.3.nupkg.sha512"
+ },
+ "Newtonsoft.Json.Bson/1.0.2": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==",
+ "path": "newtonsoft.json.bson/1.0.2",
+ "hashPath": "newtonsoft.json.bson.1.0.2.nupkg.sha512"
+ },
+ "SharpZipLib/1.4.2": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-yjj+3zgz8zgXpiiC3ZdF/iyTBbz2fFvMxZFEBPUcwZjIvXOf37Ylm+K58hqMfIBt5JgU/Z2uoUS67JmTLe973A==",
+ "path": "sharpziplib/1.4.2",
+ "hashPath": "sharpziplib.1.4.2.nupkg.sha512"
+ },
+ "Swashbuckle.AspNetCore/7.2.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-vJv19UpWm6OOgnS9QLDnWARNVasXUfj8SFvlG7UVALm4nBnfwRnEky7C0veSDqMUmBeMPC6Ec3d6G1ts/J04Uw==",
+ "path": "swashbuckle.aspnetcore/7.2.0",
+ "hashPath": "swashbuckle.aspnetcore.7.2.0.nupkg.sha512"
+ },
+ "Swashbuckle.AspNetCore.Swagger/7.2.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-y27fNDfIh1vGhJjXYynLcZjl7DLOW1bSO2MDsY9wB4Zm1fdxpPsuBSiR4U+0acWlAqLmnuOPKr/OeOgwRUkBlw==",
+ "path": "swashbuckle.aspnetcore.swagger/7.2.0",
+ "hashPath": "swashbuckle.aspnetcore.swagger.7.2.0.nupkg.sha512"
+ },
+ "Swashbuckle.AspNetCore.SwaggerGen/7.2.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-pMrTxGVuXM7t4wqft5CNNU8A0++Yw5kTLmYhB6tbEcyBfO8xEF/Y8pkJhO6BZ/2MYONrRYoQTfPFJqu8fOf5WQ==",
+ "path": "swashbuckle.aspnetcore.swaggergen/7.2.0",
+ "hashPath": "swashbuckle.aspnetcore.swaggergen.7.2.0.nupkg.sha512"
+ },
+ "Swashbuckle.AspNetCore.SwaggerUI/7.2.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-hgrXeKzyp5OGN8qVvL7A+vhmU7mDJTfGpiMBRL66IcfLOyna8UTLtn3cC3CghamXpRDufcc9ciklTszUGEQK0w==",
+ "path": "swashbuckle.aspnetcore.swaggerui/7.2.0",
+ "hashPath": "swashbuckle.aspnetcore.swaggerui.7.2.0.nupkg.sha512"
+ },
+ "System.Collections.Immutable/7.0.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-dQPcs0U1IKnBdRDBkrCTi1FoajSTBzLcVTpjO4MBCMC7f4pDOIPzgBoX8JjG7X6uZRJ8EBxsi8+DR1JuwjnzOQ==",
+ "path": "system.collections.immutable/7.0.0",
+ "hashPath": "system.collections.immutable.7.0.0.nupkg.sha512"
+ },
+ "System.Diagnostics.DiagnosticSource/9.0.1": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-yOcDWx4P/s1I83+7gQlgQLmhny2eNcU0cfo1NBWi+en4EAI38Jau+/neT85gUW6w1s7+FUJc2qNOmmwGLIREqA==",
+ "path": "system.diagnostics.diagnosticsource/9.0.1",
+ "hashPath": "system.diagnostics.diagnosticsource.9.0.1.nupkg.sha512"
+ },
+ "System.Reflection.Metadata/7.0.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-MclTG61lsD9sYdpNz9xsKBzjsmsfCtcMZYXz/IUr2zlhaTaABonlr1ESeompTgM+Xk+IwtGYU7/voh3YWB/fWw==",
+ "path": "system.reflection.metadata/7.0.0",
+ "hashPath": "system.reflection.metadata.7.0.0.nupkg.sha512"
+ },
+ "System.Runtime/4.3.1": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-abhfv1dTK6NXOmu4bgHIONxHyEqFjW8HwXPmpY9gmll+ix9UNo4XDcmzJn6oLooftxNssVHdJC1pGT9jkSynQg==",
+ "path": "system.runtime/4.3.1",
+ "hashPath": "system.runtime.4.3.1.nupkg.sha512"
+ },
+ "System.Runtime.CompilerServices.Unsafe/6.0.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==",
+ "path": "system.runtime.compilerservices.unsafe/6.0.0",
+ "hashPath": "system.runtime.compilerservices.unsafe.6.0.0.nupkg.sha512"
+ },
+ "System.Text.Encodings.Web/8.0.0": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-yev/k9GHAEGx2Rg3/tU6MQh4HGBXJs70y7j1LaM1i/ER9po+6nnQ6RRqTJn1E7Xu0fbIFK80Nh5EoODxrbxwBQ==",
+ "path": "system.text.encodings.web/8.0.0",
+ "hashPath": "system.text.encodings.web.8.0.0.nupkg.sha512"
+ },
+ "System.Text.Json/8.0.5": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-0f1B50Ss7rqxXiaBJyzUu9bWFOO2/zSlifZ/UNMdiIpDYe4cY4LQQicP4nirK1OS31I43rn062UIJ1Q9bpmHpg==",
+ "path": "system.text.json/8.0.5",
+ "hashPath": "system.text.json.8.0.5.nupkg.sha512"
+ },
+ "System.Text.RegularExpressions/4.3.1": {
+ "type": "package",
+ "serviceable": true,
+ "sha512": "sha512-N0kNRrWe4+nXOWlpLT4LAY5brb8caNFlUuIRpraCVMDLYutKkol1aV079rQjLuSxKMJT2SpBQsYX9xbcTMmzwg==",
+ "path": "system.text.regularexpressions/4.3.1",
+ "hashPath": "system.text.regularexpressions.4.3.1.nupkg.sha512"
+ },
+ "Admin.NET.Plugin.Core/1.0.0": {
+ "type": "project",
+ "serviceable": false,
+ "sha512": ""
+ },
+ "PluginCore/2.2.5": {
+ "type": "project",
+ "serviceable": false,
+ "sha512": ""
+ },
+ "PluginCore.AspNetCore/1.4.3": {
+ "type": "project",
+ "serviceable": false,
+ "sha512": ""
+ },
+ "PluginCore.IPlugins/0.9.1": {
+ "type": "project",
+ "serviceable": false,
+ "sha512": ""
+ },
+ "PluginCore.IPlugins.AspNetCore/0.1.1": {
+ "type": "project",
+ "serviceable": false,
+ "sha512": ""
+ }
+ }
+}
\ No newline at end of file
diff --git a/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/Admin.NET.Plugin.Pay.Alipay.dll b/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/Admin.NET.Plugin.Pay.Alipay.dll
new file mode 100644
index 00000000..9a087f5c
Binary files /dev/null and b/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/Admin.NET.Plugin.Pay.Alipay.dll differ
diff --git a/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/Admin.NET.Plugin.Pay.Alipay.pdb b/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/Admin.NET.Plugin.Pay.Alipay.pdb
new file mode 100644
index 00000000..565448c1
Binary files /dev/null and b/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/Admin.NET.Plugin.Pay.Alipay.pdb differ
diff --git a/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/Admin.NET.Plugin.Pay.Alipay.xml b/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/Admin.NET.Plugin.Pay.Alipay.xml
new file mode 100644
index 00000000..79a4de2e
--- /dev/null
+++ b/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/Admin.NET.Plugin.Pay.Alipay.xml
@@ -0,0 +1,34 @@
+
+
+
+ Admin.NET.Plugin.Pay.Alipay
+
+
+
+
+ 在 Build 时, 将会 new Middleware(), 最终将所有 Middleware 包装为一个
+
+
+
+
+
+
+
+
+ 测试,是否运行时添加的Middleware,是否可以依赖注入
+
+
+
+
+ 系统动态插件服务
+
+
+
+
+ 获取动态插件列表
+
+
+
+
+
+
diff --git a/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/README.md b/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/README.md
new file mode 100644
index 00000000..5f9d8314
--- /dev/null
+++ b/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/README.md
@@ -0,0 +1,14 @@
+## 说明文档(可选)
+
+- [] 这是一个示例插件
+- [x] 感谢使用
+
+## API
+
+- [/SayHello](/SayHello)
+ - 通过插件在运行时添加 管道Middleware, 并拦截响应
+
+- [/api/plugins/AliWebApi](/api/plugins/AliWebApi)
+ - 通过插件Controller 响应
+
+
\ No newline at end of file
diff --git a/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/info.json b/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/info.json
new file mode 100644
index 00000000..ec71bb1f
--- /dev/null
+++ b/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/info.json
@@ -0,0 +1,8 @@
+{
+ "PluginId": "Admin.NET.Plugin.Pay.Alipay",
+ "DisplayName": "Alipay示例",
+ "Description": "这是支付宝插件",
+ "Author": "tomny",
+ "Version": "0.1.0",
+ "SupportedVersions": [ "0.0.1" ]
+}
\ No newline at end of file
diff --git a/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/settings.json b/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/settings.json
new file mode 100644
index 00000000..9a32e4c3
--- /dev/null
+++ b/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/settings.json
@@ -0,0 +1,3 @@
+{
+ "Hello": "哈哈哈哈哈或或或或或或"
+}
\ No newline at end of file
diff --git a/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/wwwroot/css/main.css b/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/wwwroot/css/main.css
new file mode 100644
index 00000000..191068ea
--- /dev/null
+++ b/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/wwwroot/css/main.css
@@ -0,0 +1,9 @@
+* {
+ margin: 0;
+ padding: 0;
+}
+
+#app {
+ width: 100%;
+ color: deepskyblue;
+}
\ No newline at end of file
diff --git a/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/wwwroot/index.html b/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/wwwroot/index.html
new file mode 100644
index 00000000..9bb783d2
--- /dev/null
+++ b/Admin.NET/Admin.NET.Web.Entry/Plugins/Admin.NET.Plugin.Pay.Alipay/wwwroot/index.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+ HelloWorldPlugin
+
+
+
+
+
+
HelloWorldPlugin!
+
插件的前端文件应当放在 wwwroot 文件夹下
+
+
+
+
\ No newline at end of file
diff --git a/Admin.NET/Admin.NET.Web.Entry/Plugins_wwwroot/Admin.NET.Plugin.Pay.Alipay/css/main.css b/Admin.NET/Admin.NET.Web.Entry/Plugins_wwwroot/Admin.NET.Plugin.Pay.Alipay/css/main.css
new file mode 100644
index 00000000..191068ea
--- /dev/null
+++ b/Admin.NET/Admin.NET.Web.Entry/Plugins_wwwroot/Admin.NET.Plugin.Pay.Alipay/css/main.css
@@ -0,0 +1,9 @@
+* {
+ margin: 0;
+ padding: 0;
+}
+
+#app {
+ width: 100%;
+ color: deepskyblue;
+}
\ No newline at end of file
diff --git a/Admin.NET/Admin.NET.Web.Entry/Plugins_wwwroot/Admin.NET.Plugin.Pay.Alipay/index.html b/Admin.NET/Admin.NET.Web.Entry/Plugins_wwwroot/Admin.NET.Plugin.Pay.Alipay/index.html
new file mode 100644
index 00000000..9bb783d2
--- /dev/null
+++ b/Admin.NET/Admin.NET.Web.Entry/Plugins_wwwroot/Admin.NET.Plugin.Pay.Alipay/index.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+ HelloWorldPlugin
+
+
+
+
+
+
HelloWorldPlugin!
+
插件的前端文件应当放在 wwwroot 文件夹下
+
+
+
+
\ No newline at end of file
diff --git a/Admin.NET/Admin.NET.sln b/Admin.NET/Admin.NET.sln
index 998fd368..af32a0c1 100644
--- a/Admin.NET/Admin.NET.sln
+++ b/Admin.NET/Admin.NET.sln
@@ -32,6 +32,24 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Admin.NET.Plugin.PaddleOCR"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Admin.NET.Plugin.WorkWeixin", "Plugins\Admin.NET.Plugin.WorkWeixin\Admin.NET.Plugin.WorkWeixin.csproj", "{12998618-A875-4580-B5B1-0CC50CE85F27}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "PluginCore", "PluginCore", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluginCore", "Plugins\PluginCore\PluginCore\PluginCore.csproj", "{D6A36A77-53EB-222C-B7E7-AA2D81F1A9B0}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluginCore.AspNetCore", "Plugins\PluginCore\PluginCore.AspNetCore\PluginCore.AspNetCore.csproj", "{D66359C8-1F82-669F-A515-5A7F6A65DB8B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluginCore.IPlugins", "Plugins\PluginCore\PluginCore.IPlugins\PluginCore.IPlugins.csproj", "{9E4C9BFE-E657-F410-9283-CAE6C370F264}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PluginCore.IPlugins.AspNetCore", "Plugins\PluginCore\PluginCore.IPlugins.AspNetCore\PluginCore.IPlugins.AspNetCore.csproj", "{BAB9ACF0-5AEE-290C-6D33-30712B6ADF14}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test", "Test", "{3F5B6AE4-951D-4358-AF94-288C4D484D48}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Admin.NET.Plugin.Pay.Alipay", "Plugins\PluginCore\Test\Admin.NET.Plugin.Pay.Alipay\Admin.NET.Plugin.Pay.Alipay.csproj", "{0A92F217-3412-053B-AD13-E0018B024DEE}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Admin.NET.Plugin.PluginCoreManager", "Plugins\Admin.NET.Plugin.PluginCoreManager\Admin.NET.Plugin.PluginCoreManager.csproj", "{07C1B5BD-0A9F-A112-F257-0C6E4D0CC961}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Admin.NET.Plugin.Core", "Plugins\Admin.NET.Plugin.Core\Admin.NET.Plugin.Core.csproj", "{B78CD743-15E4-4295-1759-277929D38DF0}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -82,6 +100,34 @@ Global
{12998618-A875-4580-B5B1-0CC50CE85F27}.Debug|Any CPU.Build.0 = Debug|Any CPU
{12998618-A875-4580-B5B1-0CC50CE85F27}.Release|Any CPU.ActiveCfg = Release|Any CPU
{12998618-A875-4580-B5B1-0CC50CE85F27}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D6A36A77-53EB-222C-B7E7-AA2D81F1A9B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D6A36A77-53EB-222C-B7E7-AA2D81F1A9B0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D6A36A77-53EB-222C-B7E7-AA2D81F1A9B0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D6A36A77-53EB-222C-B7E7-AA2D81F1A9B0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D66359C8-1F82-669F-A515-5A7F6A65DB8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D66359C8-1F82-669F-A515-5A7F6A65DB8B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D66359C8-1F82-669F-A515-5A7F6A65DB8B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D66359C8-1F82-669F-A515-5A7F6A65DB8B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9E4C9BFE-E657-F410-9283-CAE6C370F264}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9E4C9BFE-E657-F410-9283-CAE6C370F264}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9E4C9BFE-E657-F410-9283-CAE6C370F264}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9E4C9BFE-E657-F410-9283-CAE6C370F264}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BAB9ACF0-5AEE-290C-6D33-30712B6ADF14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BAB9ACF0-5AEE-290C-6D33-30712B6ADF14}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BAB9ACF0-5AEE-290C-6D33-30712B6ADF14}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BAB9ACF0-5AEE-290C-6D33-30712B6ADF14}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0A92F217-3412-053B-AD13-E0018B024DEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0A92F217-3412-053B-AD13-E0018B024DEE}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0A92F217-3412-053B-AD13-E0018B024DEE}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0A92F217-3412-053B-AD13-E0018B024DEE}.Release|Any CPU.Build.0 = Release|Any CPU
+ {07C1B5BD-0A9F-A112-F257-0C6E4D0CC961}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {07C1B5BD-0A9F-A112-F257-0C6E4D0CC961}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {07C1B5BD-0A9F-A112-F257-0C6E4D0CC961}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {07C1B5BD-0A9F-A112-F257-0C6E4D0CC961}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B78CD743-15E4-4295-1759-277929D38DF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B78CD743-15E4-4295-1759-277929D38DF0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B78CD743-15E4-4295-1759-277929D38DF0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B78CD743-15E4-4295-1759-277929D38DF0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -94,6 +140,15 @@ Global
{9EB9C39E-E14F-443E-9AA3-EE417ABCBC1D} = {76F70D22-8D53-468E-A3B6-1704666A1D71}
{1B106C11-E5BF-44AB-A283-1E948A8BD8C2} = {76F70D22-8D53-468E-A3B6-1704666A1D71}
{12998618-A875-4580-B5B1-0CC50CE85F27} = {76F70D22-8D53-468E-A3B6-1704666A1D71}
+ {02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {76F70D22-8D53-468E-A3B6-1704666A1D71}
+ {D6A36A77-53EB-222C-B7E7-AA2D81F1A9B0} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
+ {D66359C8-1F82-669F-A515-5A7F6A65DB8B} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
+ {9E4C9BFE-E657-F410-9283-CAE6C370F264} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
+ {BAB9ACF0-5AEE-290C-6D33-30712B6ADF14} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
+ {3F5B6AE4-951D-4358-AF94-288C4D484D48} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
+ {0A92F217-3412-053B-AD13-E0018B024DEE} = {3F5B6AE4-951D-4358-AF94-288C4D484D48}
+ {07C1B5BD-0A9F-A112-F257-0C6E4D0CC961} = {76F70D22-8D53-468E-A3B6-1704666A1D71}
+ {B78CD743-15E4-4295-1759-277929D38DF0} = {76F70D22-8D53-468E-A3B6-1704666A1D71}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5CD801D7-984A-4F5C-8FA2-211B7A5EA9F3}
diff --git a/Admin.NET/Plugins/Admin.NET.Plugin.Core/Abstractions/Pay/IPayPlugin.cs b/Admin.NET/Plugins/Admin.NET.Plugin.Core/Abstractions/Pay/IPayPlugin.cs
new file mode 100644
index 00000000..b8e872c0
--- /dev/null
+++ b/Admin.NET/Plugins/Admin.NET.Plugin.Core/Abstractions/Pay/IPayPlugin.cs
@@ -0,0 +1,20 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+
+using PluginCore.IPlugins;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Admin.NET.Plugin.Core.Abstractions.Pay;
+public interface IPayPlugin : IPlugin
+{
+ string Name { get; set; }
+ string Say();
+}
diff --git a/Admin.NET/Plugins/Admin.NET.Plugin.Core/Admin.NET.Plugin.Core.csproj b/Admin.NET/Plugins/Admin.NET.Plugin.Core/Admin.NET.Plugin.Core.csproj
new file mode 100644
index 00000000..f3688bb2
--- /dev/null
+++ b/Admin.NET/Plugins/Admin.NET.Plugin.Core/Admin.NET.Plugin.Core.csproj
@@ -0,0 +1,27 @@
+
+
+
+ net8.0;net9.0
+ 1701;1702;8616;1591;8618;8619;8629;8602;8603;8604;8625;8765
+ Admin.NET.Plugin.Core.xml
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Admin.NET/Plugins/Admin.NET.Plugin.Core/Admin.NET.Plugin.Core.xml b/Admin.NET/Plugins/Admin.NET.Plugin.Core/Admin.NET.Plugin.Core.xml
new file mode 100644
index 00000000..e1f8a56e
--- /dev/null
+++ b/Admin.NET/Plugins/Admin.NET.Plugin.Core/Admin.NET.Plugin.Core.xml
@@ -0,0 +1,8 @@
+
+
+
+ Admin.NET.Plugin.Core
+
+
+
+
diff --git a/Admin.NET/Plugins/Admin.NET.Plugin.Core/GlobalUsing.cs b/Admin.NET/Plugins/Admin.NET.Plugin.Core/GlobalUsing.cs
new file mode 100644
index 00000000..19a76f13
--- /dev/null
+++ b/Admin.NET/Plugins/Admin.NET.Plugin.Core/GlobalUsing.cs
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Admin.NET.Plugin.PluginCoreManager.csproj b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Admin.NET.Plugin.PluginCoreManager.csproj
new file mode 100644
index 00000000..c9629cd6
--- /dev/null
+++ b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Admin.NET.Plugin.PluginCoreManager.csproj
@@ -0,0 +1,35 @@
+
+
+
+ net8.0;net9.0
+ 1701;1702;1591;8632
+ enable
+ disable
+ True
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
+
+
+
+
diff --git a/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Controllers/AppCenterController.cs b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Controllers/AppCenterController.cs
new file mode 100644
index 00000000..be83e4d6
--- /dev/null
+++ b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Controllers/AppCenterController.cs
@@ -0,0 +1,207 @@
+//===================================================
+// License: Apache-2.0
+// Contributors: yiyungent@gmail.com
+// Project: https://moeci.com/PluginCore
+// GitHub: https://github.com/yiyungent/PluginCore
+//===================================================
+
+
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+using PluginCore;
+using PluginCore.Models;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using PluginCore.AspNetCore.Authorization;
+using PluginCore.AspNetCore.ResponseModel;
+//using ResponseModel;
+
+
+namespace PluginCore.AspNetCore.Controllers
+{
+ ///
+ /// 应用中心
+ /// 插件
+ ///
+ [Route("api/plugincore/admin/[controller]/[action]")]
+ // [PluginCoreAdminAuthorize]
+ [ApiController]
+ [NonUnify]
+
+ public class AppCenterController : ControllerBase
+ {
+ #region Fields
+
+ private static Dictionary _pluginDownloadTasks;
+
+ #endregion
+
+ #region Ctor
+
+ static AppCenterController()
+ {
+ _pluginDownloadTasks = new Dictionary();
+ }
+
+ public AppCenterController()
+ {
+
+ }
+ #endregion
+
+ #region Actions
+
+ #region 插件列表
+ ///
+ /// 插件
+ ///
+ ///
+ ///
+ [HttpGet, HttpPost]
+ public async Task> Plugins(string query = "")
+ {
+ BaseResponseModel responseDTO = new BaseResponseModel();
+ IList pluginRegistryModels = new List();
+ try
+ {
+ // 1. TODO: 从json文件中读取插件订阅源 registry url
+ string registryUrl = "";
+ // 2. TODO: 向订阅源发送 http get 获取插件列表信息 eg: http://rem-core-plugins-registry.moeci.com/?query=xxx
+ IList remotePluginIds = new List();
+
+ // 3. 根据本地已有 PluginId 插件情况 状态赋值
+ PluginConfigModel pluginConfigModel = PluginConfigModelFactory.Create();
+ // IList localPluginIds = pluginConfigModel.EnabledPlugins.Concat(pluginConfigModel.DisabledPlugins).Concat(pluginConfigModel.UninstalledPlugins).ToList();
+ IList localPluginIds = PluginPathProvider.AllPluginFolderName();
+
+
+
+ responseDTO.Code = 1;
+ responseDTO.Message = "获取远程插件数据成功";
+ responseDTO.Data = pluginRegistryModels;
+ }
+ catch (Exception ex)
+ {
+ responseDTO.Code = -1;
+ responseDTO.Message = "获取远程插件数据失败: " + ex.Message;
+ responseDTO.Data = pluginRegistryModels;
+ }
+
+ return await Task.FromResult(responseDTO);
+ }
+ #endregion
+
+ #region 下载插件
+ [HttpGet, HttpPost]
+ public async Task> DownloadPlugin(string pluginDownloadUrl = "")
+ {
+ BaseResponseModel responseDTO = new BaseResponseModel();
+
+ #region 效验
+ if (string.IsNullOrEmpty(pluginDownloadUrl))
+ {
+ responseDTO.Code = -1;
+ responseDTO.Message = "插件下载地址不正确";
+ return responseDTO;
+ }
+ // TODO: 效验是否本地已经存在相同pluginId的插件
+
+ #endregion
+
+ try
+ {
+ // 1.执行下载操作, TODO:存在问题,阻塞对性能不好,但不阻塞又不好通知用户插件下载进度,以及可能存在在插件下载过程中,用户再次点击下载
+ WebClient webClient = new WebClient();
+ // TODO: 插件下载文件路径
+ string pluginDownloadFilePath = "";
+ //webClient.DownloadFileAsync(new Uri(pluginDownloadFilePath), "");
+ Task task = webClient.DownloadFileTaskAsync(pluginDownloadUrl, pluginDownloadFilePath);
+
+ _pluginDownloadTasks.Add(pluginDownloadUrl, task);
+
+ webClient.DownloadFileCompleted += Plugin_DownloadFileCompleted;
+ webClient.DownloadProgressChanged += Plugin_DownloadProgressChanged;
+ webClient.Disposed += WebClient_Disposed;
+
+ responseDTO.Code = 1;
+ responseDTO.Message = "开始下载插件";
+ }
+ catch (Exception ex)
+ {
+ responseDTO.Code = -1;
+ responseDTO.Message = "下载插件失败: " + ex.Message;
+ }
+
+ return await Task.FromResult(responseDTO);
+ }
+
+
+
+
+ #endregion
+
+ #region 获取插件下载进度
+ [HttpGet, HttpPost]
+ public async Task> DownloadPluginProgress()
+ {
+ BaseResponseModel responseDTO = new BaseResponseModel();
+ try
+ {
+ responseDTO.Data = new { };
+
+
+
+ responseDTO.Code = 1;
+ responseDTO.Message = "获取插件下载进度成功";
+ }
+ catch (Exception ex)
+ {
+ responseDTO.Code = -1;
+ responseDTO.Message = "获取插件下载进度失败: " + ex.Message;
+ }
+
+ return await Task.FromResult(responseDTO);
+ }
+ #endregion
+
+ #endregion
+
+ #region Helpers
+
+ ///
+ /// 插件下载完成
+ ///
+ ///
+ ///
+ private void Plugin_DownloadFileCompleted(object sender, System.ComponentModel.AsyncCompletedEventArgs e)
+ {
+ Console.WriteLine("插件下载完成");
+ // 1.从 _pluginDownloadTasks 中移除
+ //_pluginDownloadTasks.Remove();
+ // 2. 解压插件
+
+ }
+
+ private void Plugin_DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e)
+ {
+ Console.WriteLine($"插件下载进度改变: {e.ProgressPercentage}% {e.BytesReceived}/{e.TotalBytesToReceive}");
+ }
+
+ private void WebClient_Disposed(object sender, EventArgs e)
+ {
+ if (sender is WebClient webClient)
+ {
+ Console.WriteLine(webClient.BaseAddress);
+ }
+
+ Console.WriteLine(nameof(WebClient_Disposed) + ": " + sender.ToString());
+ }
+
+ #endregion
+
+ }
+}
diff --git a/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Controllers/DebugController.cs b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Controllers/DebugController.cs
new file mode 100644
index 00000000..d64d4dff
--- /dev/null
+++ b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Controllers/DebugController.cs
@@ -0,0 +1,279 @@
+using System.Runtime.CompilerServices;
+//===================================================
+// License: Apache-2.0
+// Contributors: yiyungent@gmail.com
+// Project: https://moeci.com/PluginCore
+// GitHub: https://github.com/yiyungent/PluginCore
+//===================================================
+
+
+
+using Microsoft.AspNetCore.Mvc;
+using PluginCore.AspNetCore.Authorization;
+using PluginCore.AspNetCore.ResponseModel;
+using PluginCore.Interfaces;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using System.Linq;
+using System.Runtime.Loader;
+using Microsoft.Extensions.DependencyInjection;
+using System.Collections.Concurrent;
+using PluginCore.AspNetCore.Extensions;
+
+namespace PluginCore.AspNetCore.Controllers
+{
+ ///
+ /// [ASP.NET Core — 依赖注入\_啊晚的博客-CSDN博客\_asp.net core 依赖注入](https://blog.csdn.net/weixin_37648525/article/details/127942292)
+ /// [ASP.NET Core中的依赖注入(3): 服务的注册与提供 - Artech - 博客园](https://www.cnblogs.com/artech/p/asp-net-core-di-register.html)
+ /// [ASP.NET Core中的依赖注入(5): ServiceProvider实现揭秘 【总体设计 】 - Artech - 博客园](https://www.cnblogs.com/artech/p/asp-net-core-di-service-provider-1.html)
+ /// [dotnet/ServiceProvider.cs at main · dotnet/dotnet](https://github.com/dotnet/dotnet/blob/main/src/runtime/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceProvider.cs)
+ /// [Net6 DI源码分析Part2 Engine,ServiceProvider - 一身大膘 - 博客园](https://www.cnblogs.com/hts92/p/15800990.html)
+ /// [【特别的骚气】asp.net core运行时注入服务,实现类库热插拔 - 四处观察 - 博客园](https://www.cnblogs.com/1996-Chinese-Chen/p/16154218.html)
+ ///
+ /// ActivatorUtilities.CreateInstance(serviceProvider, "test");
+ /// ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider);
+ ///
+ ///
+ [Route("api/plugincore/admin/[controller]/[action]")]
+ //[PluginCoreAdminAuthorize]
+ [ApiController]
+ [NonUnify]
+ public class DebugController : ControllerBase
+ {
+ #region Fields
+ private readonly IPluginContextManager _pluginContextManager;
+ #endregion
+
+ #region Ctor
+ public DebugController(IPluginContextManager pluginContextManager)
+ {
+ _pluginContextManager = pluginContextManager;
+ }
+ #endregion
+
+ #region Actions
+
+ [HttpGet, HttpPost]
+ public async Task> PluginContexts()
+ {
+ BaseResponseModel responseModel = new BaseResponseModel();
+ try
+ {
+ var pluginContextList = _pluginContextManager.All();
+ Dictionary> keyValuePairs = new Dictionary>();
+ foreach (var pluginContext in pluginContextList)
+ {
+ keyValuePairs.Add($"{pluginContext.GetType().ToString()} - {pluginContext.PluginId} - {pluginContext.GetHashCode()}", pluginContext.Assemblies.Select(m => m.FullName).ToList());
+ }
+
+ responseModel.Code = 1;
+ responseModel.Message = "success";
+ responseModel.Data = keyValuePairs;
+ }
+ catch (Exception ex)
+ {
+ responseModel.Code = -1;
+ responseModel.Message = "error";
+ responseModel.Data = ex.ToString();
+ }
+
+ return await Task.FromResult(responseModel);
+ }
+
+ [HttpGet, HttpPost]
+ public async Task> AssemblyLoadContexts()
+ {
+ BaseResponseModel responseModel = new BaseResponseModel();
+ try
+ {
+ var assemblyLoadContextDefault = AssemblyLoadContext.Default;
+ var assemblyLoadContextAll = AssemblyLoadContext.All;
+ var responseDataModel = new AssemblyLoadContextsResponseDataModel();
+ responseDataModel.Default = new AssemblyLoadContextsResponseDataModel.AssemblyLoadContextModel
+ {
+ Name = assemblyLoadContextDefault.Name,
+ Type = assemblyLoadContextDefault.GetType().ToString(),
+ Assemblies = assemblyLoadContextDefault.Assemblies.Select(m => new AssemblyModel { FullName = m.FullName, DefinedTypes = m.DefinedTypes.Select(m => m.FullName).ToList() }).ToList()
+ };
+ responseDataModel.All = assemblyLoadContextAll.Select(item => new AssemblyLoadContextsResponseDataModel.AssemblyLoadContextModel
+ {
+ Name = item.Name,
+ Type = item.GetType().ToString(),
+ Assemblies = item.Assemblies.Select(m => new AssemblyModel { FullName = m.FullName, DefinedTypes = m.DefinedTypes.Select(m => m.FullName).ToList() }).ToList()
+ }).ToList();
+
+ responseModel.Code = 1;
+ responseModel.Message = "success";
+ responseModel.Data = responseDataModel;
+ }
+ catch (Exception ex)
+ {
+ responseModel.Code = -1;
+ responseModel.Message = "error";
+ responseModel.Data = ex.ToString();
+ }
+
+ return await Task.FromResult(responseModel);
+ }
+
+ [HttpGet, HttpPost]
+ public async Task> Assemblies()
+ {
+ BaseResponseModel responseModel = new BaseResponseModel();
+ try
+ {
+ var assemblies = AppDomain.CurrentDomain.GetAssemblies();
+ List assemblyModels = new List();
+ foreach (var item in assemblies)
+ {
+ assemblyModels.Add(new AssemblyModel
+ {
+ FullName = item.FullName,
+ DefinedTypes = item.DefinedTypes.Select(m => m.FullName).ToList()
+ });
+ }
+
+ responseModel.Code = 1;
+ responseModel.Message = "success";
+ responseModel.Data = assemblyModels;
+ }
+ catch (Exception ex)
+ {
+ responseModel.Code = -1;
+ responseModel.Message = "error";
+ responseModel.Data = ex.ToString();
+ }
+
+ return await Task.FromResult(responseModel);
+ }
+
+ [HttpGet, HttpPost]
+ public async Task> Services([FromServices] IServiceProvider serviceProvider)
+ {
+ BaseResponseModel responseModel = new BaseResponseModel();
+ try
+ {
+ //IServiceProvider serviceProvider = HttpContext.RequestServices;
+ //var provider = serviceProvider.GetType().GetProperty("RootProvider", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+ //var serviceField = provider.GetType().GetField("_realizedServices", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+ //var serviceValue = serviceField.GetValue(provider);
+ //var funcType = serviceField.FieldType.GetGenericArguments()[1].GetGenericArguments()[0];
+ //ConcurrentDictionary> realizedServices = (ConcurrentDictionary>)serviceValue;
+
+ // 获取所有已经注册的服务
+ var allService = serviceProvider.GetAllServiceDescriptors();
+
+ List serviceModels = new List();
+ foreach (var item in allService)
+ {
+ serviceModels.Add(new ServiceModel
+ {
+ Type = item.Key.ToString(),
+ ImplementationType = item.Value.ImplementationType?.ToString() ?? "",
+ Lifetime = item.Value.Lifetime.ToString(),
+ TypeAssembly = new AssemblyModel
+ {
+ FullName = item.Key.Assembly.FullName,
+ },
+ ImplementationTypeAssembly = new AssemblyModel
+ {
+ FullName = item.Value.ImplementationType?.Assembly?.FullName ?? ""
+ }
+ });
+ }
+
+ responseModel.Code = 1;
+ responseModel.Message = "success";
+ responseModel.Data = serviceModels;
+ }
+ catch (Exception ex)
+ {
+ responseModel.Code = -1;
+ responseModel.Message = "error";
+ responseModel.Data = ex.ToString();
+ }
+
+ return await Task.FromResult(responseModel);
+ }
+
+ #endregion
+
+ public sealed class AssemblyLoadContextsResponseDataModel
+ {
+ public AssemblyLoadContextModel Default
+ {
+ get; set;
+ }
+
+ public List All
+ {
+ get; set;
+ }
+
+ public sealed class AssemblyLoadContextModel
+ {
+ public string Name
+ {
+ get; set;
+ }
+ public string Type
+ {
+ get; set;
+ }
+ public List Assemblies
+ {
+ get; set;
+ }
+ }
+ }
+
+ public sealed class AssembliesResponseDataModel
+ {
+
+ }
+
+ public sealed class ServiceModel
+ {
+ public string Type
+ {
+ get; set;
+ }
+
+ public string ImplementationType
+ {
+ get; set;
+ }
+
+ public string Lifetime
+ {
+ get; set;
+ }
+
+ public AssemblyModel TypeAssembly
+ {
+ get; set;
+ }
+
+ public AssemblyModel ImplementationTypeAssembly
+ {
+ get; set;
+ }
+ }
+
+ public sealed class AssemblyModel
+ {
+ public string FullName
+ {
+ get; set;
+ }
+
+ public List DefinedTypes
+ {
+ get; set;
+ }
+ }
+
+ }
+}
diff --git a/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Controllers/PluginWidgetController.cs b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Controllers/PluginWidgetController.cs
new file mode 100644
index 00000000..8267dd3a
--- /dev/null
+++ b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Controllers/PluginWidgetController.cs
@@ -0,0 +1,91 @@
+//===================================================
+// License: Apache-2.0
+// Contributors: yiyungent@gmail.com
+// Project: https://moeci.com/PluginCore
+// GitHub: https://github.com/yiyungent/PluginCore
+//===================================================
+
+
+
+using PluginCore.AspNetCore.ResponseModel;
+using PluginCore.Interfaces;
+using PluginCore.IPlugins;
+
+namespace PluginCore.AspNetCore.Controllers
+{
+ [Route("api/plugincore/[controller]/[action]")]
+ [ApiController]
+ [NonUnify]
+ public class PluginWidgetController : ControllerBase
+ {
+ #region Fields
+ private readonly IPluginFinder _pluginFinder;
+ #endregion
+
+ #region Ctor
+ public PluginWidgetController(IPluginFinder pluginFinder)
+ {
+ _pluginFinder = pluginFinder;
+ }
+ #endregion
+
+ #region Actions
+
+ #region Widget
+ ///
+ /// Widget
+ ///
+ ///
+ [HttpGet, HttpPost]
+ //public async Task> Widget(string widgetKey, string extraPars = "")
+ public async Task Widget(string widgetKey, string extraPars = "")
+ {
+ BaseResponseModel responseModel = new ResponseModel.BaseResponseModel();
+ string responseData = "";
+ widgetKey = widgetKey.Trim('"', '\'');
+ string[] extraParsArr = null;
+ if (!string.IsNullOrEmpty(extraPars))
+ {
+ extraParsArr = extraPars.Split(",", StringSplitOptions.RemoveEmptyEntries);
+ extraParsArr = extraParsArr.Select(m => m.Trim('"', '\'')).ToArray();
+ }
+ StringBuilder sb = new StringBuilder();
+ sb.AppendLine($"");
+ try
+ {
+ List plugins = this._pluginFinder.EnablePlugins().ToList();
+ foreach (var item in plugins)
+ {
+ string widgetStr = await item.Widget(widgetKey, extraParsArr);
+ if (!string.IsNullOrEmpty(widgetStr))
+ {
+ // TODO: 配合 PluginCoreConfig.PluginWidgetDebug
+ // TODO: PluginCoreConfig 改为 Options 模式, 避免手动反复读取文件 效率低
+ //sb.AppendLine($"");
+
+ sb.AppendLine(widgetStr);
+ }
+ }
+
+ }
+ catch (Exception ex)
+ {
+ Utils.LogUtil.Error(ex.ToString());
+ sb.AppendLine($"");
+ }
+ sb.AppendLine($"");
+ responseData = sb.ToString();
+
+ responseModel.Code = 1;
+ responseModel.Message = "Load Widget Success";
+ responseModel.Data = responseData;
+
+ //return await Task.FromResult(responseModel);
+ return Content(responseData, "text/html;charset=utf-8");
+ }
+ #endregion
+
+ #endregion
+
+ }
+}
diff --git a/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Controllers/PluginsController.cs b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Controllers/PluginsController.cs
new file mode 100644
index 00000000..98fb2553
--- /dev/null
+++ b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Controllers/PluginsController.cs
@@ -0,0 +1,680 @@
+//===================================================
+// License: Apache-2.0
+// Contributors: yiyungent@gmail.com
+// Project: https://moeci.com/PluginCore
+// GitHub: https://github.com/yiyungent/PluginCore
+//===================================================
+
+
+
+using System;
+using System.Collections.Generic;
+using System.Configuration;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+//using Core.Common;
+//using Framework.Authorization;
+using PluginCore;
+using PluginCore.Models;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using PluginCore.AspNetCore.Authorization;
+using PluginCore.Infrastructure;
+using PluginCore.IPlugins;
+using PluginCore.AspNetCore.ResponseModel;
+using PluginCore.Interfaces;
+using PluginCore.AspNetCore.Interfaces;
+
+//using ResponseModel;
+
+namespace PluginCore.AspNetCore.Controllers
+{
+ [Route("api/plugincore/admin/[controller]/[action]")]
+ // [PluginCoreAdminAuthorize]
+ [ApiController]
+ [NonUnify]
+ public class PluginsController : ControllerBase
+ {
+ #region Fields
+ private readonly IPluginManager _pluginManager;
+ private readonly IPluginFinder _pluginFinder;
+ private readonly IPluginApplicationBuilderManager _pluginApplicationBuilderManager;
+ #endregion
+
+ #region Ctor
+ public PluginsController(IPluginManager pluginManager, IPluginFinder pluginFinder, IPluginApplicationBuilderManager pluginApplicationBuilderManager)
+ {
+ _pluginManager = pluginManager;
+ _pluginFinder = pluginFinder;
+ _pluginApplicationBuilderManager = pluginApplicationBuilderManager;
+ }
+ #endregion
+
+ #region Actions
+
+ #region 插件列表
+ ///
+ /// 加载插件列表
+ ///
+ /// 插件状态
+ ///
+ [HttpGet, HttpPost]
+ public async Task> List(string status = "all")
+ {
+ BaseResponseModel responseData = new ResponseModel.BaseResponseModel();
+ var pluginConfigModel = PluginConfigModelFactory.Create();
+
+ // 获取所有插件信息
+ IList pluginInfoModels = PluginInfoModelFactory.CreateAll();
+ IList responseModels = new List();
+ string[] enablePluginIds = _pluginFinder.EnablePluginIds().ToArray();
+
+ // 添加插件状态
+ responseModels = PluginInfoModelToResponseModel(pluginInfoModels, pluginConfigModel, enablePluginIds);
+ #region 筛选插件状态
+ switch (status.ToLower())
+ {
+ case "all":
+ break;
+ case "enabled":
+ responseModels = responseModels.Where(m => m.Status == PluginStatus.Enabled).ToList();
+ break;
+ case "disabled":
+ responseModels = responseModels.Where(m => m.Status == PluginStatus.Disabled).ToList();
+ break;
+ default:
+ break;
+ }
+ #endregion
+
+ responseData.Code = 1;
+ responseData.Message = "加载插件列表成功";
+ responseData.Data = responseModels;
+
+ return await Task.FromResult(responseData);
+ }
+ #endregion
+
+ #region 卸载插件
+ [HttpGet, HttpPost]
+ public async Task> Uninstall(string pluginId)
+ {
+ BaseResponseModel responseData = new BaseResponseModel();
+ pluginId = pluginId.Trim();
+ var pluginConfigModel = PluginConfigModelFactory.Create();
+ // 卸载插件 必须 先禁用插件
+ #region 效验
+ if (pluginConfigModel.EnabledPlugins.Contains(pluginId))
+ {
+ responseData.Code = -1;
+ responseData.Message = "卸载失败: 请先禁用此插件";
+ return await Task.FromResult(responseData);
+ }
+ string pluginDirStr= Path.Combine(PluginPathProvider.PluginsRootPath(), pluginId);
+ string pluginWwwrootDirStr = Path.Combine(PluginPathProvider.PluginsWwwRootDir(), pluginId);
+ if (!Directory.Exists(pluginDirStr) && !Directory.Exists(pluginWwwrootDirStr))
+ {
+ responseData.Code = -2;
+ responseData.Message = "卸载失败: 此插件不存在";
+ return await Task.FromResult(responseData);
+ }
+ #endregion
+
+ try
+ {
+ // PS:卸载插件必须先禁用插件,所以此时插件LoadContext已被移除释放(插件Assemblies已被释放), 此处不需移除LoadContext
+
+ // 1.删除物理文件
+ var pluginDir = new DirectoryInfo(pluginDirStr);
+ if (pluginDir.Exists) {
+ pluginDir.Delete(true);
+ }
+ // 虽然 已禁用 时 pluginWwwrootDirStr/pluginId 已删除, 但为确保, 还是再删除一次
+ var pluginWwwrootDir = new DirectoryInfo(pluginWwwrootDirStr);
+ if (pluginWwwrootDir.Exists) {
+ pluginWwwrootDir.Delete(true);
+ }
+
+ responseData.Code = 1;
+ responseData.Message = "卸载成功";
+ }
+ catch (Exception ex)
+ {
+ responseData.Code = -1;
+ responseData.Message = "卸载失败: " + ex.Message;
+ }
+
+ return await Task.FromResult(responseData);
+ }
+ #endregion
+
+ #region 启用插件
+ [HttpGet, HttpPost]
+ public async Task> Enable(string pluginId)
+ {
+ BaseResponseModel responseData = new BaseResponseModel();
+ var pluginConfigModel = PluginConfigModelFactory.Create();
+ // 效验是否存在于 已禁用插件列表
+ #region 效验
+ pluginId = pluginId.Trim();
+ var pluginDir = new DirectoryInfo(Path.Combine(PluginPathProvider.PluginsRootPath(), pluginId));
+ if (pluginDir != null && !pluginDir.Exists)
+ {
+ responseData.Code = -1;
+ responseData.Message = "启用失败: 此插件不存在";
+ return await Task.FromResult(responseData);
+ }
+ string[] enablePluginIds = _pluginFinder.EnablePluginIds().ToArray();
+ if (enablePluginIds.Contains(pluginId)) {
+ responseData.Code = -2;
+ responseData.Message = "启用失败: 此插件已启用";
+ return await Task.FromResult(responseData);
+ }
+ #endregion
+
+ try
+ {
+ // 1. 创建插件程序集加载上下文, 添加到 PluginsLoadContexts
+ _pluginManager.LoadPlugin(pluginId);
+ // 2. 添加到 pluginConfigModel.EnabledPlugins
+ pluginConfigModel.EnabledPlugins.Add(pluginId);
+ // 4.保存到 plugin.config.json
+ PluginConfigModelFactory.Save(pluginConfigModel);
+
+ // 5. 找到此插件实例
+ IPlugin plugin = _pluginFinder.Plugin(pluginId);
+ if (plugin == null)
+ {
+ // 7.启用不成功, 回滚插件状态: (1)释放插件上下文 (2)更新 plugin.config.json
+ try
+ {
+ _pluginManager.UnloadPlugin(pluginId);
+ }
+ catch (Exception ex)
+ { }
+
+ // 从 pluginConfigModel.EnabledPlugins 移除
+ pluginConfigModel.EnabledPlugins.Remove(pluginId);
+ // 保存到 plugin.config.json
+ PluginConfigModelFactory.Save(pluginConfigModel);
+
+ responseData.Code = -1;
+ responseData.Message = "启用失败: 此插件不存在";
+ return await Task.FromResult(responseData);
+ }
+ // 6.调取插件的 AfterEnable(), 插件开发者可在此回收资源
+ var pluginEnableResult = plugin.AfterEnable();
+ if (!pluginEnableResult.IsSuccess)
+ {
+ // 7.启用不成功, 回滚插件状态: (1)释放插件上下文 (2)更新 plugin.config.json
+ try
+ {
+ _pluginManager.UnloadPlugin(pluginId);
+ }
+ catch (Exception ex)
+ { }
+
+ // 从 pluginConfigModel.EnabledPlugins 移除
+ pluginConfigModel.EnabledPlugins.Remove(pluginId);
+ // 保存到 plugin.config.json
+ PluginConfigModelFactory.Save(pluginConfigModel);
+
+ responseData.Code = -1;
+ responseData.Message = "启用失败: 来自插件的错误信息: " + pluginEnableResult.Message;
+ return await Task.FromResult(responseData);
+ }
+
+ // 7. ReBuild
+ this._pluginApplicationBuilderManager.ReBuild();
+
+ // 8. 尝试复制 插件下的 wwwroot 到 Plugins_wwwroot
+ string wwwRootDir = PluginPathProvider.WwwRootDir(pluginId);
+ if (Directory.Exists(wwwRootDir))
+ {
+ string targetDir = PluginPathProvider.PluginWwwRootDir(pluginId);
+ Utils.FileUtil.CopyFolder(wwwRootDir, targetDir);
+ }
+
+ responseData.Code = 1;
+ responseData.Message = "启用成功";
+ }
+ catch (Exception ex)
+ {
+ responseData.Code = -2;
+ responseData.Message = "启用失败: " + ex.Message;
+ }
+
+ return await Task.FromResult(responseData);
+ }
+ #endregion
+
+ #region 禁用插件
+ [HttpGet, HttpPost]
+ public async Task> Disable(string pluginId)
+ {
+ BaseResponseModel responseData = new BaseResponseModel();
+ #region 效验
+ pluginId = pluginId.Trim();
+ var pluginConfigModel = PluginConfigModelFactory.Create();
+ // string[] enablePluginIds = _pluginFinder.EnablePluginIds().ToArray();
+ // // 效验是否存在于 已启用插件列表
+ // if (!enablePluginIds.Contains(pluginId))
+ // {
+ // responseData.Code = -1;
+ // responseData.Message = "禁用失败: 此插件不存在, 或未启用";
+ // return await Task.FromResult(responseData);
+ // }
+ #endregion
+
+ try
+ {
+ // 1. 找到此插件实例
+ IPlugin plugin = _pluginFinder.Plugin(pluginId);
+ if (plugin == null)
+ {
+ responseData.Code = -1;
+ responseData.Message = "禁用失败: 此插件不存在, 或未启用";
+ return await Task.FromResult(responseData);
+ }
+ try
+ {
+ // 2.调取插件的 BeforeDisable(), 插件开发者可在此回收资源
+ var pluginDisableResult = plugin.BeforeDisable();
+ if (!pluginDisableResult.IsSuccess)
+ {
+ responseData.Code = -1;
+ responseData.Message = "禁用失败: 来自插件的错误信息: " + pluginDisableResult.Message;
+ return await Task.FromResult(responseData);
+ }
+ // 3.移除插件对应的程序集加载上下文
+ _pluginManager.UnloadPlugin(pluginId);
+ // 3.1. ReBuild
+ this._pluginApplicationBuilderManager.ReBuild();
+ if (pluginConfigModel.EnabledPlugins.Contains(pluginId)) {
+ // 4.从 pluginConfigModel.EnabledPlugins 移除
+ pluginConfigModel.EnabledPlugins.Remove(pluginId);
+ // 5.保存到 plugin.config.json
+ PluginConfigModelFactory.Save(pluginConfigModel);
+ }
+ }
+ catch (Exception ex)
+ {
+ Utils.LogUtil.Error(ex.ToString());
+ responseData.Code = -1;
+ responseData.Message = "禁用失败: 此插件不存在, 或未启用";
+ return await Task.FromResult(responseData);
+ }
+
+ // 7. 尝试移除 Plugins_wwwroot/PluginId
+ string pluginWwwRootDir = PluginPathProvider.PluginWwwRootDir(pluginId);
+ if (Directory.Exists(pluginWwwRootDir))
+ {
+ Directory.Delete(pluginWwwRootDir, true);
+ }
+
+
+ responseData.Code = 1;
+ responseData.Message = "禁用成功";
+ }
+ catch (Exception ex)
+ {
+ responseData.Code = -2;
+ responseData.Message = "禁用失败: " + ex.Message;
+ }
+
+ return await Task.FromResult(responseData);
+ }
+ #endregion
+
+ #region 上传插件
+ ///
+ /// 上传插件
+ ///
+ /// 注意: 参数名一定为 file, 对应前端传过来时以 file 为名
+ ///
+ [HttpGet, HttpPost]
+ public async Task> Upload([FromForm] IFormFile file)
+ {
+ BaseResponseModel responseData = new BaseResponseModel();
+
+ #region 效验
+ if (file == null)
+ {
+ responseData.Code = -1;
+ responseData.Message = "上传的文件不能为空";
+ return responseData;
+ }
+ //文件后缀
+ string fileExtension = Path.GetExtension(file.FileName);//获取文件格式,拓展名
+ // 类型标记
+ UploadFileType uploadFileType = UploadFileType.NoAllowedType;
+ switch (fileExtension)
+ {
+ case ".zip":
+ uploadFileType = UploadFileType.Zip;
+ break;
+ case ".nupkg":
+ uploadFileType = UploadFileType.Nupkg;
+ break;
+ }
+
+ if (fileExtension != ".zip" && fileExtension != ".nupkg")
+ {
+ responseData.Code = -1;
+ // nupkg 其实就是 zip
+ responseData.Message = "只能上传 zip 或 nupkg 格式文件";
+ return responseData;
+ }
+ // PluginCore.AspNetCore-v1.0.2 起 不再限制插件上传大小
+ //判断文件大小
+ //var fileSize = file.Length;
+ //if (fileSize > 1024 * 1024 * 5) // 5M
+ //{
+ // responseData.Code = -1;
+ // responseData.Message = "上传的文件不能大于5MB";
+ // return responseData;
+ //}
+ #endregion
+
+ try
+ {
+ // 1.先上传到 临时插件上传目录, 用Guid.zip作为保存文件名
+ string tempZipFilePath = Path.Combine(PluginPathProvider.TempPluginUploadDir(), Guid.NewGuid() + ".zip");
+ using (var fs = System.IO.File.Create(tempZipFilePath))
+ {
+ file.CopyTo(fs); //将上传的文件文件流,复制到fs中
+ fs.Flush();//清空文件流
+ }
+ // 2.解压
+ bool isDecomparessSuccess = false;
+ if (uploadFileType == UploadFileType.Zip)
+ {
+ isDecomparessSuccess = Utils.ZipHelper.DecomparessFile(tempZipFilePath, tempZipFilePath.Replace(".zip", ""));
+ }
+ else if (uploadFileType == UploadFileType.Nupkg)
+ {
+ isDecomparessSuccess = NupkgService.DecomparessFile(tempZipFilePath, tempZipFilePath.Replace(".zip", ""));
+ }
+
+ // 3.删除原压缩包
+ System.IO.File.Delete(tempZipFilePath);
+ if (!isDecomparessSuccess)
+ {
+ responseData.Code = -1;
+ responseData.Message = "解压插件压缩包失败";
+ return responseData;
+ }
+ // 4.读取其中的info.json, 获取 PluginId 值
+ PluginInfoModel pluginInfoModel = PluginInfoModelFactory.ReadPluginDir(tempZipFilePath.Replace(".zip", ""));
+ if (pluginInfoModel == null || string.IsNullOrEmpty(pluginInfoModel.PluginId))
+ {
+ // 记得删除已不再需要的临时插件文件夹
+ Directory.Delete(tempZipFilePath.Replace(".zip", ""), true);
+
+ responseData.Code = -1;
+ responseData.Message = "不合法的插件";
+ return responseData;
+ }
+ string pluginId = pluginInfoModel.PluginId;
+ // 5.检索 此 PluginId 是否本地插件已存在
+ var pluginConfigModel = PluginConfigModelFactory.Create();
+ // 本地已经存在的 PluginId
+ IList localExistPluginIds = PluginPathProvider.AllPluginFolderName();
+ if (localExistPluginIds.Contains(pluginId))
+ {
+ // 记得删除已不再需要的临时插件文件夹
+ Directory.Delete(tempZipFilePath.Replace(".zip", ""), true);
+
+ responseData.Code = -1;
+ responseData.Message = $"本地已有此插件 (PluginId: {pluginId}), 请前往插件列表删除后, 再上传";
+ return responseData;
+ }
+ // 6.本地无此插件 -> 移动插件文件夹到 Plugins 下, 并以 PluginId 为插件文件夹名
+ string pluginsRootPath = PluginPathProvider.PluginsRootPath();
+ string newPluginDir = Path.Combine(pluginsRootPath, pluginId);
+ Directory.Move(tempZipFilePath.Replace(".zip", ""), newPluginDir);
+
+ // 7. 放入 Plugins 中, 默认为 已禁用
+
+ responseData.Code = 1;
+ responseData.Message = $"上传插件成功 (PluginId: {pluginId})";
+ }
+ catch (Exception ex)
+ {
+ responseData.Code = -1;
+ responseData.Message = "上传插件失败: " + ex.Message;
+ ex = ex.InnerException;
+ while (ex != null)
+ {
+ responseData.Message += " - " + ex.InnerException.Message;
+ ex = ex.InnerException;
+ }
+ }
+
+ return await Task.FromResult(responseData);
+ }
+ #endregion
+
+ #region 查看详细
+ [HttpGet, HttpPost]
+ public async Task> Details(string pluginId)
+ {
+ BaseResponseModel responseData = new BaseResponseModel();
+
+ try
+ {
+ #region 效验
+ pluginId = pluginId.Trim();
+ var pluginConfigModel = PluginConfigModelFactory.Create();
+ string[] localPluginIds = PluginPathProvider.AllPluginFolderName().ToArray();
+
+ if (!localPluginIds.Contains(pluginId))
+ {
+ responseData.Code = -1;
+ responseData.Message = $"查看详细失败: 不存在 {pluginId} 插件";
+ return await Task.FromResult(responseData);
+ }
+
+ #endregion
+
+ PluginInfoModel pluginInfoModel = PluginInfoModelFactory.Create(pluginId);
+ string[] enablePluginIds = _pluginFinder.EnablePluginIds().ToArray();
+ PluginInfoResponseModel pluginInfoResponseModel = PluginInfoModelToResponseModel(new List() { pluginInfoModel }, pluginConfigModel, enablePluginIds).FirstOrDefault();
+
+
+ responseData.Code = 1;
+ responseData.Message = "查看详细成功";
+ responseData.Data = pluginInfoResponseModel;
+ }
+ catch (Exception ex)
+ {
+ responseData.Code = -1;
+ responseData.Message = "查看详细失败: " + ex.Message;
+ }
+
+ return await Task.FromResult(responseData);
+ }
+ #endregion
+
+ #region 查看文档
+ [HttpGet, HttpPost]
+ public async Task> Readme(string pluginId)
+ {
+ BaseResponseModel responseData = new BaseResponseModel();
+
+ try
+ {
+ #region 效验
+ pluginId = pluginId.Trim();
+ // var pluginConfigModel = PluginConfigModelFactory.Create();
+ string[] localPluginIds = PluginPathProvider.AllPluginFolderName().ToArray();
+
+ if (!localPluginIds.Contains(pluginId))
+ {
+ responseData.Code = -1;
+ responseData.Message = $"查看文档失败: 不存在 {pluginId} 插件";
+ return await Task.FromResult(responseData);
+ }
+
+ #endregion
+
+ PluginReadmeModel readmeModel = PluginReadmeModelFactory.Create(pluginId);
+ PluginReadmeResponseModel readmeResponseModel = new PluginReadmeResponseModel();
+ readmeResponseModel.Content = readmeModel?.Content ?? "";
+ readmeResponseModel.PluginId = pluginId;
+
+ responseData.Code = 1;
+ responseData.Message = "查看文档成功";
+ responseData.Data = readmeResponseModel;
+ }
+ catch (Exception ex)
+ {
+ responseData.Code = -1;
+ responseData.Message = "查看文档失败: " + ex.Message;
+ }
+
+ return await Task.FromResult(responseData);
+ }
+ #endregion
+
+ #region 设置
+ [HttpGet]
+ public async Task> Settings(string pluginId)
+ {
+ BaseResponseModel responseData = new BaseResponseModel();
+
+ try
+ {
+ #region 效验
+ pluginId = pluginId.Trim();
+ // var pluginConfigModel = PluginConfigModelFactory.Create();
+ string[] localPluginIds = PluginPathProvider.AllPluginFolderName().ToArray();
+
+ if (!localPluginIds.Contains(pluginId))
+ {
+ responseData.Code = -1;
+ responseData.Message = $"查看设置失败: 不存在 {pluginId} 插件";
+ return await Task.FromResult(responseData);
+ }
+
+ #endregion
+
+ string settingsJsonStr = PluginSettingsModelFactory.Create(pluginId);
+
+
+ responseData.Code = 1;
+ responseData.Message = "查看设置成功";
+ responseData.Data = settingsJsonStr ?? "无设置项";
+ }
+ catch (Exception ex)
+ {
+ responseData.Code = -1;
+ responseData.Message = "查看设置失败: " + ex.Message;
+ }
+
+ return await Task.FromResult(responseData);
+ }
+
+ [HttpPost]
+ public async Task> Settings(PluginSettingsInputModel inputModel)
+ {
+ BaseResponseModel responseData = new BaseResponseModel();
+
+ try
+ {
+ #region 效验
+ inputModel.PluginId = inputModel.PluginId.Trim();
+ // var pluginConfigModel = PluginConfigModelFactory.Create();
+ string[] localPluginIds = PluginPathProvider.AllPluginFolderName().ToArray();
+
+ if (!localPluginIds.Contains(inputModel.PluginId))
+ {
+ responseData.Code = -1;
+ responseData.Message = $"设置失败: 不存在 {inputModel.PluginId} 插件";
+ return await Task.FromResult(responseData);
+ }
+
+ #endregion
+
+ inputModel.Data = inputModel.Data ?? "";
+ PluginSettingsModelFactory.Save(pluginSettingsJsonStr: inputModel.Data, pluginId: inputModel.PluginId);
+
+
+ responseData.Code = 1;
+ responseData.Message = "设置成功";
+ responseData.Data = inputModel.Data;
+ }
+ catch (Exception ex)
+ {
+ responseData.Code = -1;
+ responseData.Message = "设置失败: " + ex.Message;
+ }
+
+ return await Task.FromResult(responseData);
+ }
+ #endregion
+
+ #endregion
+
+
+ #region Helpers
+
+ [NonAction]
+ private IList PluginInfoModelToResponseModel(IList pluginInfoModels, PluginConfigModel pluginConfigModel, string[] enablePluginIds)
+ {
+ // 获取 Plugins 下所有插件
+ // DirectoryInfo pluginsDir = new DirectoryInfo(PluginPathProvider.PluginsRootPath());
+ // List pluginIds = pluginsDir?.GetDirectories()?.Select(m => m.Name)?.ToList() ?? new List();
+
+ IList responseModels = new List();
+ #region 添加插件状态信息
+ foreach (var model in pluginInfoModels)
+ {
+ PluginInfoResponseModel responseModel = new PluginInfoResponseModel();
+ responseModel.Author = model.Author;
+ responseModel.Description = model.Description;
+ responseModel.DisplayName = model.DisplayName;
+ responseModel.PluginId = model.PluginId;
+ responseModel.SupportedVersions = model.SupportedVersions;
+ responseModel.Version = model.Version;
+ responseModel.DependPlugins = model.DependPlugins;
+
+ if (pluginConfigModel.EnabledPlugins.Contains(model.PluginId) && !enablePluginIds.Contains(model.PluginId)) {
+ // 错误情况: 配置 标识 已启用, 但实际没有启用成功
+ pluginConfigModel.EnabledPlugins.Remove(model.PluginId);
+ PluginConfigModelFactory.Save(pluginConfigModel);
+
+ responseModel.Status = PluginStatus.Disabled;
+ } else if(!pluginConfigModel.EnabledPlugins.Contains(model.PluginId) && enablePluginIds.Contains(model.PluginId))
+ {
+ // 错误情况: 配置没有标识 已启用, 但实际 已启用
+ pluginConfigModel.EnabledPlugins.Add(model.PluginId);
+ PluginConfigModelFactory.Save(pluginConfigModel);
+
+ responseModel.Status = PluginStatus.Enabled;
+ } else if (pluginConfigModel.EnabledPlugins.Contains(model.PluginId) && enablePluginIds.Contains(model.PluginId))
+ {
+ responseModel.Status = PluginStatus.Enabled;
+ }
+ else
+ {
+ responseModel.Status = PluginStatus.Disabled;
+ }
+ responseModels.Add(responseModel);
+ }
+ #endregion
+
+ return responseModels;
+ }
+
+ public enum UploadFileType
+ {
+ NoAllowedType = 0,
+ Zip = 1,
+ Nupkg = 2
+ }
+
+ #endregion
+ }
+}
diff --git a/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Controllers/UserController.cs b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Controllers/UserController.cs
new file mode 100644
index 00000000..fea34d0b
--- /dev/null
+++ b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Controllers/UserController.cs
@@ -0,0 +1,204 @@
+//===================================================
+// License: Apache-2.0
+// Contributors: yiyungent@gmail.com
+// Project: https://moeci.com/PluginCore
+// GitHub: https://github.com/yiyungent/PluginCore
+//===================================================
+
+
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc;
+using PluginCore.AspNetCore.Authorization;
+using PluginCore.Config;
+using PluginCore.AspNetCore.RequestModel.User;
+using PluginCore.AspNetCore.ResponseModel;
+using Microsoft.AspNetCore.Authorization;
+using Admin.NET.Core.Service;
+using Admin.NET.Core;
+using Furion.DataEncryption;
+using Furion.FriendlyException;
+using Lazy.Captcha.Core;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel;
+
+namespace PluginCore.AspNetCore.Controllers
+{
+ [Route("api/plugincore/admin/[controller]/[action]")]
+ [ApiController]
+ [NonUnify]
+ public class UserController : ControllerBase
+ {
+
+
+ public string RemoteFronted
+ {
+ get
+ {
+ return PluginCore.Config.PluginCoreConfigFactory.Create().RemoteFrontend;
+ }
+ }
+
+ private readonly IUserManager _userManager;
+ private readonly SqlSugarRepository _sysUserRep;
+ private readonly SysOrgService _sysOrgService;
+ private readonly SysUserExtOrgService _sysUserExtOrgService;
+ private readonly SysUserRoleService _sysUserRoleService;
+ private readonly SysConfigService _sysConfigService;
+
+ public UserController(IUserManager userManager,
+ SqlSugarRepository sysUserRep,
+ SysOrgService sysOrgService,
+ SysUserExtOrgService sysUserExtOrgService,
+ SysUserRoleService sysUserRoleService,
+ SysConfigService sysConfigService)
+ {
+ _userManager = userManager;
+ _sysUserRep = sysUserRep;
+ _sysOrgService = sysOrgService;
+ _sysUserExtOrgService = sysUserExtOrgService;
+ _sysUserRoleService = sysUserRoleService;
+ _sysConfigService = sysConfigService;
+ }
+ ///
+ /// 登录系统
+ ///
+ ///
+ /// 用户名/密码:superadmin/123456
+ ///
+ [AllowAnonymous]
+ [HttpGet, HttpPost]
+ [DisplayName("登录系统")]
+ public async Task> Login([FromBody] LoginRequestModel input)
+ {
+ BaseResponseModel responseModel = new BaseResponseModel();
+
+
+ // 账号是否存在
+ var user = await _sysUserRep.AsQueryable().Includes(t => t.SysOrg).Filter(null, true).FirstAsync(u => u.Account.Equals(input.UserName));
+ _ = user ?? throw Oops.Oh(ErrorCodeEnum.D0009);
+
+ // 账号是否被冻结
+ if (user.Status == StatusEnum.Disable)
+ throw Oops.Oh(ErrorCodeEnum.D1017);
+
+ // 租户是否被禁用
+ var tenant = await _sysUserRep.ChangeRepository>().GetFirstAsync(u => u.Id == user.TenantId);
+ if (tenant != null && tenant.Status == StatusEnum.Disable)
+ throw Oops.Oh(ErrorCodeEnum.Z1003);
+
+ // 密码是否正确
+ if (CryptogramUtil.CryptoType == CryptogramEnum.MD5.ToString())
+ {
+ if (user.Password != MD5Encryption.Encrypt(input.Password))
+ throw Oops.Oh(ErrorCodeEnum.D1000);
+ }
+ else
+ {
+ if (CryptogramUtil.Decrypt(user.Password) != input.Password)
+ throw Oops.Oh(ErrorCodeEnum.D1000);
+ }
+
+
+ var tokenExpire = await _sysConfigService.GetTokenExpire();
+ var refreshTokenExpire = await _sysConfigService.GetRefreshTokenExpire();
+
+ // 生成Token令牌
+ var accessToken = JWTEncryption.Encrypt(new Dictionary
+ {
+ { ClaimConst.UserId, user.Id },
+ { ClaimConst.TenantId, user.TenantId },
+ { ClaimConst.Account, user.Account },
+ { ClaimConst.RealName, user.RealName },
+ { ClaimConst.AccountType, user.AccountType },
+ { ClaimConst.OrgId, user.OrgId },
+ { ClaimConst.OrgName, user.SysOrg?.Name },
+ { ClaimConst.OrgType, user.SysOrg?.OrgType },
+ }, tokenExpire);
+
+ // 生成刷新Token令牌
+ var refreshToken = JWTEncryption.GenerateRefreshToken(accessToken, refreshTokenExpire);
+
+ responseModel.Code = 1;
+ responseModel.Message = "登录成功";
+ responseModel.Data = new
+ {
+ token = accessToken,
+ userName = user.NickName,
+ RefreshToken = refreshToken
+ };
+ return await Task.FromResult(responseModel);
+
+ }
+
+
+ [HttpGet, HttpPost]
+ public async Task> Logout()
+ {
+ BaseResponseModel responseModel = new BaseResponseModel()
+ {
+ Code = 1,
+ Message = "退出登录成功"
+ };
+
+ return await Task.FromResult(responseModel);
+ }
+
+
+ [HttpGet, HttpPost]
+ public async Task> Info()
+ {
+ BaseResponseModel responseModel = new BaseResponseModel();
+
+ try
+ {
+ string adminUserName = PluginCoreConfigFactory.Create().Admin.UserName;
+
+ responseModel.Code = 1;
+ responseModel.Message = "成功";
+ responseModel.Data = new
+ {
+ name = adminUserName,
+ //avatar = this.RemoteFronted + "/images/avatar.gif"
+ avatar = ""
+ };
+ }
+ catch (Exception ex)
+ {
+ responseModel.Code = -1;
+ responseModel.Message = "失败: " + ex.Message;
+ }
+
+ return await Task.FromResult(responseModel);
+ }
+
+
+ [HttpGet, HttpPost]
+ public async Task> Update([FromBody] UpdateRequestModel requestModel)
+ {
+ BaseResponseModel responseModel = new BaseResponseModel();
+
+ try
+ {
+ PluginCoreConfig pluginCoreConfig = PluginCoreConfigFactory.Create();
+ pluginCoreConfig.Admin.UserName = requestModel.UserName;
+ pluginCoreConfig.Admin.Password = requestModel.Password;
+ PluginCoreConfigFactory.Save(pluginCoreConfig);
+
+ responseModel.Code = 1;
+ responseModel.Message = "修改成功, 需要重新登录";
+ }
+ catch (Exception ex)
+ {
+ responseModel.Code = -1;
+ responseModel.Message = "失败: " + ex.Message;
+ }
+
+ return await Task.FromResult(responseModel);
+ }
+
+ }
+}
diff --git a/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Entity/SysPluginCore.cs b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Entity/SysPluginCore.cs
new file mode 100644
index 00000000..899047dd
--- /dev/null
+++ b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Entity/SysPluginCore.cs
@@ -0,0 +1,73 @@
+// 麻省理工学院许可证
+//
+// 版权所有 (c) 2021-2023 zuohuaijun,大名科技(天津)有限公司 联系电话/微信:18020030720 QQ:515096995
+//
+// 特此免费授予获得本软件的任何人以处理本软件的权利,但须遵守以下条件:在所有副本或重要部分的软件中必须包括上述版权声明和本许可声明。
+//
+// 软件按“原样”提供,不提供任何形式的明示或暗示的保证,包括但不限于对适销性、适用性和非侵权的保证。
+// 在任何情况下,作者或版权持有人均不对任何索赔、损害或其他责任负责,无论是因合同、侵权或其他方式引起的,与软件或其使用或其他交易有关。
+
+
+namespace Admin.NET.Plugin.PluginCoreManager;
+
+///
+/// 系统动态插件表
+///
+[SugarTable(null, "系统动态插件表")]
+[SysTable]
+public class SysPluginCore : EntityTenant
+{
+ ///
+ /// 插件ID
+ ///
+ [SugarColumn(ColumnDescription = "插件ID", Length = 128)]
+ [Required]
+ public virtual string PluginId { get; set; }
+
+ ///
+ /// 名称
+ ///
+ [SugarColumn(ColumnDescription = "名称", Length = 64)]
+ [Required, MaxLength(64)]
+ public virtual string DisplayName { get; set; }
+
+ ///
+ /// 作者
+ ///
+ [SugarColumn(ColumnDescription = "作者", Length = 64)]
+ [Required, MaxLength(64)]
+ public virtual string Author { get; set; }
+
+ ///
+ /// 版本
+ ///
+ [SugarColumn(ColumnDescription = "版本", Length = 64)]
+ [Required, MaxLength(64)]
+ public virtual string Version { get; set; }
+
+ ///
+ /// 描述
+ ///
+ [SugarColumn(ColumnDescription = "描述", Length = 512)]
+ [MaxLength(512)]
+ public string? Description { get; set; }
+
+ ///
+ /// 排序
+ ///
+ [SugarColumn(ColumnDescription = "排序")]
+ public int OrderNo { get; set; } = 100;
+
+ ///
+ /// 状态
+ ///
+ [SugarColumn(ColumnDescription = "状态")]
+ public StatusEnum Status { get; set; } = StatusEnum.Enable;
+
+ ///
+ /// 备注
+ ///
+ [SugarColumn(ColumnDescription = "备注", Length = 128)]
+ [MaxLength(128)]
+ public string? Remark { get; set; }
+}
\ No newline at end of file
diff --git a/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/GlobalUsings.cs b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/GlobalUsings.cs
new file mode 100644
index 00000000..11d11f46
--- /dev/null
+++ b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/GlobalUsings.cs
@@ -0,0 +1,25 @@
+// 麻省理工学院许可证
+//
+// 版权所有 (c) 2021-2023 zuohuaijun,大名科技(天津)有限公司 联系电话/微信:18020030720 QQ:515096995
+//
+// 特此免费授予获得本软件的任何人以处理本软件的权利,但须遵守以下条件:在所有副本或重要部分的软件中必须包括上述版权声明和本许可声明。
+//
+// 软件按“原样”提供,不提供任何形式的明示或暗示的保证,包括但不限于对适销性、适用性和非侵权的保证。
+// 在任何情况下,作者或版权持有人均不对任何索赔、损害或其他责任负责,无论是因合同、侵权或其他方式引起的,与软件或其使用或其他交易有关。
+global using Admin.NET.Core;
+global using Admin.NET.Core.Service;
+global using Furion;
+global using Furion.DependencyInjection;
+global using Furion.DynamicApiController;
+global using Furion.FriendlyException;
+global using Mapster;
+global using Microsoft.AspNetCore.Http;
+global using Microsoft.AspNetCore.Mvc;
+global using Microsoft.Extensions.DependencyInjection;
+global using Newtonsoft.Json;
+global using SqlSugar;
+global using System.ComponentModel;
+global using System.ComponentModel.DataAnnotations;
+global using System.Data;
+global using System.Linq.Dynamic.Core;
+global using System.Text;
diff --git a/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/SeedData/SysMenuSeedData.cs b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/SeedData/SysMenuSeedData.cs
new file mode 100644
index 00000000..ade0beba
--- /dev/null
+++ b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/SeedData/SysMenuSeedData.cs
@@ -0,0 +1,39 @@
+// 麻省理工学院许可证
+//
+// 版权所有 (c) 2021-2023 zuohuaijun,大名科技(天津)有限公司 联系电话/微信:18020030720 QQ:515096995
+//
+// 特此免费授予获得本软件的任何人以处理本软件的权利,但须遵守以下条件:在所有副本或重要部分的软件中必须包括上述版权声明和本许可声明。
+//
+// 软件按“原样”提供,不提供任何形式的明示或暗示的保证,包括但不限于对适销性、适用性和非侵权的保证。
+// 在任何情况下,作者或版权持有人均不对任何索赔、损害或其他责任负责,无论是因合同、侵权或其他方式引起的,与软件或其使用或其他交易有关。
+
+namespace Admin.NET.Plugin.PluginCoreManager;
+
+///
+/// 系统菜单表种子数据
+///
+[IncreSeed]
+public class SysMenu_PluginCore_SeedData : ISqlSugarEntitySeedData
+{
+ ///
+ /// 种子数据
+ ///
+ ///
+ public IEnumerable HasData()
+ {
+ return new[]
+ {
+
+ // 建议此处Id范围之间放置具体业务应用菜单
+ new SysMenu{ Id=1310000000802, Pid=1310000000301, Title="应用插件", Path="/platform/plugincore", Name="plugincore", Component="/system/plugincore/index", Icon="ele-TurnOff", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=210 },
+ new SysMenu{ Id=1310000000803, Pid=1310000000802, Title="启用", Permission="sysPluginCore/enable", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=212 },
+ new SysMenu{ Id=1310000000804, Pid=1310000000802, Title="卸载", Permission="sysPluginCore/delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=213 },
+ new SysMenu{ Id=1310000000805, Pid=1310000000802, Title="禁用", Permission="sysPluginCore/disable", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=214 },
+ new SysMenu{ Id=1310000000806, Pid=1310000000802, Title="详细", Permission="sysPluginCore/details", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=215 },
+ new SysMenu{ Id=1310000000807, Pid=1310000000802, Title="文档", Permission="sysPluginCore/readme", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=216 },
+ new SysMenu{ Id=1310000000808, Pid=1310000000802, Title="设置", Permission="sysPluginCore/setting", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=217 },
+
+
+ };
+ }
+}
\ No newline at end of file
diff --git a/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/SeedData/SysRoleMenuSeedData.cs b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/SeedData/SysRoleMenuSeedData.cs
new file mode 100644
index 00000000..6a7b8556
--- /dev/null
+++ b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/SeedData/SysRoleMenuSeedData.cs
@@ -0,0 +1,39 @@
+// 麻省理工学院许可证
+//
+// 版权所有 (c) 2021-2023 zuohuaijun,大名科技(天津)有限公司 联系电话/微信:18020030720 QQ:515096995
+//
+// 特此免费授予获得本软件的任何人以处理本软件的权利,但须遵守以下条件:在所有副本或重要部分的软件中必须包括上述版权声明和本许可声明。
+//
+// 软件按“原样”提供,不提供任何形式的明示或暗示的保证,包括但不限于对适销性、适用性和非侵权的保证。
+// 在任何情况下,作者或版权持有人均不对任何索赔、损害或其他责任负责,无论是因合同、侵权或其他方式引起的,与软件或其使用或其他交易有关。
+
+namespace Admin.NET.Core;
+
+///
+/// 系统角色菜单表种子数据
+///
+[IncreSeed]
+public class SysRoleMenu_PluginCore_SeedData : ISqlSugarEntitySeedData
+{
+ ///
+ /// 种子数据
+ ///
+ ///
+ public IEnumerable HasData()
+ {
+ return new[]
+ {
+
+ // 应用插件
+
+ new SysRoleMenu{ Id=1300000000831, RoleId=1300000000101, MenuId=1310000000802 },
+ new SysRoleMenu{ Id=1300000000832, RoleId=1300000000101, MenuId=1310000000803 },
+ new SysRoleMenu{ Id=1300000000833, RoleId=1300000000101, MenuId=1310000000803 },
+ new SysRoleMenu{ Id=1300000000834, RoleId=1300000000101, MenuId=1310000000804 },
+ new SysRoleMenu{ Id=1300000000835, RoleId=1300000000101, MenuId=1310000000805 },
+ new SysRoleMenu{ Id=1300000000836, RoleId=1300000000101, MenuId=1310000000806 },
+ new SysRoleMenu{ Id=1300000000837, RoleId=1300000000101, MenuId=1310000000807 },
+ new SysRoleMenu{ Id=1300000000830, RoleId=1300000000101, MenuId=1310000000808 },
+ };
+ }
+}
\ No newline at end of file
diff --git a/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Service/Plugin/Dto/PluginInput.cs b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Service/Plugin/Dto/PluginInput.cs
new file mode 100644
index 00000000..6530a282
--- /dev/null
+++ b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Service/Plugin/Dto/PluginInput.cs
@@ -0,0 +1,43 @@
+// 麻省理工学院许可证
+//
+// 版权所有 (c) 2021-2023 zuohuaijun,大名科技(天津)有限公司 联系电话/微信:18020030720 QQ:515096995
+//
+// 特此免费授予获得本软件的任何人以处理本软件的权利,但须遵守以下条件:在所有副本或重要部分的软件中必须包括上述版权声明和本许可声明。
+//
+// 软件按“原样”提供,不提供任何形式的明示或暗示的保证,包括但不限于对适销性、适用性和非侵权的保证。
+// 在任何情况下,作者或版权持有人均不对任何索赔、损害或其他责任负责,无论是因合同、侵权或其他方式引起的,与软件或其使用或其他交易有关。
+
+namespace Admin.NET.Plugin.PluginCoreManager.Service.Plugin.Dto;
+
+public class PagePluginCoreInput : BasePageInput
+{
+ ///
+ /// 名称
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// 编码
+ ///
+ public string Code { get; set; }
+}
+
+public class AddPluginCoreInput : SysPluginCore
+{
+
+}
+
+public class UpdatePluginCoreInput : AddPluginCoreInput
+{
+}
+
+public class DeletePluginCoreInput : BaseIdInput
+{
+}
+public class EnablePluginCoreInput : BaseIdInput
+{
+}
+public class UpdatePluginCoreSettingInput : BaseIdInput
+{
+ public string Data { get; set; }
+}
\ No newline at end of file
diff --git a/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Service/Plugin/SysPluginCoreService.cs b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Service/Plugin/SysPluginCoreService.cs
new file mode 100644
index 00000000..cb94f581
--- /dev/null
+++ b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Service/Plugin/SysPluginCoreService.cs
@@ -0,0 +1,729 @@
+// 麻省理工学院许可证
+//
+// 版权所有 (c) 2021-2023 zuohuaijun,大名科技(天津)有限公司 联系电话/微信:18020030720 QQ:515096995
+//
+// 特此免费授予获得本软件的任何人以处理本软件的权利,但须遵守以下条件:在所有副本或重要部分的软件中必须包括上述版权声明和本许可声明。
+//
+// 软件按“原样”提供,不提供任何形式的明示或暗示的保证,包括但不限于对适销性、适用性和非侵权的保证。
+// 在任何情况下,作者或版权持有人均不对任何索赔、损害或其他责任负责,无论是因合同、侵权或其他方式引起的,与软件或其使用或其他交易有关。
+
+using Admin.NET.Core;
+using Admin.NET.Plugin.PluginCoreManager.Service.Plugin.Dto;
+using Furion.VirtualFileServer;
+using Minio;
+using OnceMi.AspNetCore.OSS;
+using Org.BouncyCastle.Asn1.Ocsp;
+using PluginCore;
+using PluginCore.AspNetCore.Interfaces;
+using PluginCore.AspNetCore.ResponseModel;
+using PluginCore.Infrastructure;
+using PluginCore.Interfaces;
+using PluginCore.IPlugins;
+using PluginCore.Models;
+
+namespace Admin.NET.Plugin.PluginCoreManager.Service.Plugin;
+
+///
+/// 系统动态插件服务
+///
+[ApiDescriptionSettings(Order = 245)]
+public class SysPluginCoreService : IDynamicApiController, ITransient
+{
+ #region Fields
+ private readonly IPluginManager _pluginManager;
+ private readonly IPluginFinder _pluginFinder;
+ private readonly IPluginApplicationBuilderManager _pluginApplicationBuilderManager;
+ #endregion
+ private readonly IDynamicApiRuntimeChangeProvider _provider;
+ private readonly SqlSugarRepository _sysPluginRep;
+
+ public SysPluginCoreService(IPluginManager pluginManager, IPluginFinder pluginFinder, IPluginApplicationBuilderManager pluginApplicationBuilderManager, IDynamicApiRuntimeChangeProvider provider,
+ SqlSugarRepository sysPluginRep)
+ {
+ _pluginManager = pluginManager;
+ _pluginFinder = pluginFinder;
+ _pluginApplicationBuilderManager = pluginApplicationBuilderManager;
+ _provider = provider;
+ _sysPluginRep = sysPluginRep;
+ }
+
+ ///
+ /// 获取动态插件列表
+ ///
+ ///
+ ///
+ [DisplayName("获取动态插件列表")]
+ public async Task> Page(PagePluginInput input)
+ {
+ return await _sysPluginRep.AsQueryable()
+ .WhereIF(!string.IsNullOrWhiteSpace(input.Name), u => u.DisplayName.Contains(input.Name))
+ .OrderBy(u => u.OrderNo)
+ .ToPagedListAsync(input.Page, input.PageSize);
+ }
+
+
+ ///
+ /// 查看详细
+ ///
+ ///
+ ///
+ [ApiDescriptionSettings(Name = "Details"), HttpGet]
+ [DisplayName("查看详细")]
+ public async Task Details(long id)
+ {
+ var pluginCore = await _sysPluginRep.GetByIdAsync(id);
+ if (pluginCore == null) throw Oops.Oh("查询插件ID失败"); ;
+
+ // 先移除再添加动态程序集/接口
+
+ try
+ {
+ #region 效验
+ var pluginId = pluginCore.PluginId;
+ var pluginConfigModel = PluginConfigModelFactory.Create();
+ string[] localPluginIds = PluginPathProvider.AllPluginFolderName().ToArray();
+
+ if (!localPluginIds.Contains(pluginId))
+ {
+ throw Oops.Oh($"查看详细失败: 不存在 {pluginId} 插件");
+
+ }
+
+ #endregion
+
+ PluginInfoModel pluginInfoModel = PluginInfoModelFactory.Create(pluginId);
+ string[] enablePluginIds = _pluginFinder.EnablePluginIds().ToArray();
+ PluginInfoResponseModel pluginInfoResponseModel = PluginInfoModelToResponseModel(new List() { pluginInfoModel }, pluginConfigModel, enablePluginIds).FirstOrDefault();
+
+
+ return pluginInfoResponseModel;
+ }
+ catch (Exception ex)
+ {
+ throw Oops.Oh("查看详细失败: " + ex.Message);
+
+ }
+
+ }
+
+ #region 查看文档
+ ///
+ /// 查看文档
+ ///
+ ///
+ ///
+ [ApiDescriptionSettings(Name = "Readme"), HttpGet]
+ [DisplayName("查看文档")]
+ public async Task Readme(long id)
+ {
+ var pluginCore = await _sysPluginRep.GetByIdAsync(id);
+ if (pluginCore == null) throw Oops.Oh("查询插件ID失败"); ;
+
+
+ try
+ {
+ #region 效验
+ var pluginId = pluginCore.PluginId;
+ var pluginConfigModel = PluginConfigModelFactory.Create();
+ string[] localPluginIds = PluginPathProvider.AllPluginFolderName().ToArray();
+
+ if (!localPluginIds.Contains(pluginId))
+ {
+ throw Oops.Oh($"查看详细失败: 不存在 {pluginId} 插件");
+
+ }
+
+ #endregion
+
+ PluginReadmeModel readmeModel = PluginReadmeModelFactory.Create(pluginId);
+ PluginReadmeResponseModel readmeResponseModel = new PluginReadmeResponseModel();
+ readmeResponseModel.Content = readmeModel?.Content ?? "";
+ readmeResponseModel.PluginId = pluginId;
+
+
+ return readmeResponseModel;
+
+ }
+ catch (Exception ex)
+ {
+ throw Oops.Oh("查看详细失败: " + ex.Message);
+
+ }
+
+
+ }
+ #endregion
+
+ ///
+ /// 卸载动态插件
+ ///
+ ///
+ ///
+ [ApiDescriptionSettings(Name = "Uninstall"), HttpPost]
+ [DisplayName("卸载动态插件")]
+ public async Task Uninstall(DeletePluginCoreInput input)
+ {
+ var pluginCore = await _sysPluginRep.GetByIdAsync(input.Id);
+ if (pluginCore == null) throw Oops.Oh("查询插件ID失败");
+
+
+
+ // 卸载插件 必须 先禁用插件
+ #region 效验
+ var pluginId = pluginCore.PluginId;
+ var pluginConfigModel = PluginConfigModelFactory.Create();
+ if (pluginConfigModel.EnabledPlugins.Contains(pluginId))
+ {
+
+ throw Oops.Oh("卸载失败: 请先禁用此插件");
+ }
+ string pluginDirStr = Path.Combine(PluginPathProvider.PluginsRootPath(), pluginId);
+ string pluginWwwrootDirStr = Path.Combine(PluginPathProvider.PluginsWwwRootDir(), pluginId);
+ if (!Directory.Exists(pluginDirStr) && !Directory.Exists(pluginWwwrootDirStr))
+ {
+
+ throw Oops.Oh("卸载失败: 此插件不存在");
+ }
+ #endregion
+
+ try
+ {
+ // PS:卸载插件必须先禁用插件,所以此时插件LoadContext已被移除释放(插件Assemblies已被释放), 此处不需移除LoadContext
+
+ // 1.删除物理文件
+ var pluginDir = new DirectoryInfo(pluginDirStr);
+ if (pluginDir.Exists)
+ {
+ pluginDir.Delete(true);
+ }
+ // 虽然 已禁用 时 pluginWwwrootDirStr/pluginId 已删除, 但为确保, 还是再删除一次
+ var pluginWwwrootDir = new DirectoryInfo(pluginWwwrootDirStr);
+ if (pluginWwwrootDir.Exists)
+ {
+ pluginWwwrootDir.Delete(true);
+ }
+
+ await _sysPluginRep.DeleteAsync(u => u.Id == input.Id);
+ }
+ catch (Exception ex)
+ {
+
+ throw Oops.Oh("卸载失败: " + ex.Message);
+ }
+
+
+
+ }
+
+ #region 设置
+ ///
+ /// 插件设置设置
+ ///
+ ///
+ ///
+ [ApiDescriptionSettings(Name = "Settings"), HttpGet]
+ [DisplayName("插件设置设置")]
+ public async Task Settings(long id)
+ {
+ var pluginCore = await _sysPluginRep.GetByIdAsync(id);
+ if (pluginCore == null) throw Oops.Oh("查询插件ID失败"); ;
+
+ try
+ {
+ #region 效验
+ var pluginId = pluginCore.PluginId;
+ var pluginConfigModel = PluginConfigModelFactory.Create();
+ string[] localPluginIds = PluginPathProvider.AllPluginFolderName().ToArray();
+
+ if (!localPluginIds.Contains(pluginId))
+ {
+ throw Oops.Oh($"查看详细失败: 不存在 {pluginId} 插件");
+
+ }
+
+ #endregion
+
+ string settingsJsonStr = PluginSettingsModelFactory.Create(pluginId);
+
+
+
+ return settingsJsonStr ?? "无设置项";
+ }
+ catch (Exception ex)
+ {
+ throw Oops.Oh("查看设置失败: " + ex.Message);
+
+ }
+
+ }
+ ///
+ /// 插件设置设置
+ ///
+ ///
+ ///
+ [ApiDescriptionSettings(Name = "Settings"), HttpPost]
+ [DisplayName("插件设置设置")]
+ public async Task Settings(UpdatePluginCoreSettingInput input)
+ {
+ var pluginCore = await _sysPluginRep.GetByIdAsync(input.Id);
+ if (pluginCore == null) throw Oops.Oh("查询插件ID失败"); ;
+
+ try
+ {
+ #region 效验
+ var pluginId = pluginCore.PluginId;
+ var pluginConfigModel = PluginConfigModelFactory.Create();
+ string[] localPluginIds = PluginPathProvider.AllPluginFolderName().ToArray();
+
+ if (!localPluginIds.Contains(pluginId))
+ {
+ throw Oops.Oh($"查看详细失败: 不存在 {pluginId} 插件");
+
+ }
+
+ #endregion
+
+ input.Data = input.Data ?? "";
+ PluginSettingsModelFactory.Save(pluginSettingsJsonStr: input.Data, pluginId: pluginCore.PluginId);
+
+
+
+ }
+ catch (Exception ex)
+ {
+ throw Oops.Oh("设置失败: " + ex.Message);
+
+
+ }
+
+
+ }
+ #endregion
+
+
+ ///
+ /// 启用插件
+ ///
+ ///
+ ///
+ [ApiDescriptionSettings(Name = "Enable"), HttpPost]
+ [DisplayName("启用插件")]
+ public async Task Enable(EnablePluginCoreInput input)
+ {
+ var pluginCore = await _sysPluginRep.GetByIdAsync(input.Id);
+ if (pluginCore == null) return;
+
+ // 移除动态程序集/接口
+ var pluginConfigModel = PluginConfigModelFactory.Create();
+ // 效验是否存在于 已禁用插件列表
+ #region 效验
+ var pluginId = pluginCore.PluginId;
+ var pluginDir = new DirectoryInfo(Path.Combine(PluginPathProvider.PluginsRootPath(), pluginId));
+ if (pluginDir != null && !pluginDir.Exists)
+ {
+ throw Oops.Oh("启用失败: 此插件不存在");
+
+ }
+ string[] enablePluginIds = _pluginFinder.EnablePluginIds().ToArray();
+ if (enablePluginIds.Contains(pluginId))
+ {
+ throw Oops.Oh("启用失败: 此插件已启用");
+
+ }
+ #endregion
+
+ try
+ {
+ // 1. 创建插件程序集加载上下文, 添加到 PluginsLoadContexts
+ _pluginManager.LoadPlugin(pluginId);
+ // 2. 添加到 pluginConfigModel.EnabledPlugins
+ pluginConfigModel.EnabledPlugins.Add(pluginId);
+ // 4.保存到 plugin.config.json
+ PluginConfigModelFactory.Save(pluginConfigModel);
+
+ // 5. 找到此插件实例
+ IPlugin plugin = _pluginFinder.Plugin(pluginId);
+ if (plugin == null)
+ {
+ // 7.启用不成功, 回滚插件状态: (1)释放插件上下文 (2)更新 plugin.config.json
+ try
+ {
+ _pluginManager.UnloadPlugin(pluginId);
+ }
+ catch (Exception ex)
+ { }
+
+ // 从 pluginConfigModel.EnabledPlugins 移除
+ pluginConfigModel.EnabledPlugins.Remove(pluginId);
+ // 保存到 plugin.config.json
+ PluginConfigModelFactory.Save(pluginConfigModel);
+
+ throw Oops.Oh("启用失败: 此插件不存在");
+
+
+ }
+ // 6.调取插件的 AfterEnable(), 插件开发者可在此回收资源
+ var pluginEnableResult = plugin.AfterEnable();
+ if (!pluginEnableResult.IsSuccess)
+ {
+ // 7.启用不成功, 回滚插件状态: (1)释放插件上下文 (2)更新 plugin.config.json
+ try
+ {
+ _pluginManager.UnloadPlugin(pluginId);
+ }
+ catch (Exception ex)
+ { }
+
+ // 从 pluginConfigModel.EnabledPlugins 移除
+ pluginConfigModel.EnabledPlugins.Remove(pluginId);
+ // 保存到 plugin.config.json
+ PluginConfigModelFactory.Save(pluginConfigModel);
+ throw Oops.Oh("启用失败: 来自插件的错误信息: " + pluginEnableResult.Message);
+
+
+ }
+
+ // 7. ReBuild
+ this._pluginApplicationBuilderManager.ReBuild();
+
+ // 8. 尝试复制 插件下的 wwwroot 到 Plugins_wwwroot
+ string wwwRootDir = PluginPathProvider.WwwRootDir(pluginId);
+ if (Directory.Exists(wwwRootDir))
+ {
+ string targetDir = PluginPathProvider.PluginWwwRootDir(pluginId);
+ PluginCore.Utils.FileUtil.CopyFolder(wwwRootDir, targetDir);
+ }
+
+ //9.载入Furion动态插件
+ var pluginMainAssembly = _pluginManager.GetPluginAssembly(pluginId);
+ // 将程序集添加进动态 WebAPI 应用部件
+ _provider.AddAssembliesWithNotifyChanges(pluginMainAssembly);
+
+ await _sysPluginRep.UpdateAsync(u => new SysPluginCore() { Status = StatusEnum.Enable }, u => u.Id == input.Id);
+ }
+ catch (Exception ex)
+ {
+ throw Oops.Oh("启用失败: " + ex.Message);
+
+ }
+
+
+ }
+ ///
+ /// 禁用插件
+ ///
+ ///
+ ///
+ [ApiDescriptionSettings(Name = "Disable"), HttpPost]
+ [DisplayName("禁用插件")]
+ public async Task Disable(EnablePluginCoreInput input)
+ {
+ var pluginCore = await _sysPluginRep.GetByIdAsync(input.Id);
+ if (pluginCore == null) return;
+
+ // 移除动态程序集/
+ #region 效验
+ var pluginId = pluginCore.PluginId;
+ var pluginConfigModel = PluginConfigModelFactory.Create();
+ // string[] enablePluginIds = _pluginFinder.EnablePluginIds().ToArray();
+ // // 效验是否存在于 已启用插件列表
+ // if (!enablePluginIds.Contains(pluginId))
+ // {
+ // responseData.Code = -1;
+ // responseData.Message = "禁用失败: 此插件不存在, 或未启用";
+ // return await Task.FromResult(responseData);
+ // }
+ #endregion
+
+ try
+ {
+ // 1. 找到此插件实例
+ IPlugin plugin = _pluginFinder.Plugin(pluginId);
+ if (plugin == null)
+ {
+
+ throw Oops.Oh("禁用失败: 此插件不存在, 或未启用");
+ }
+ try
+ {
+ // 2.调取插件的 BeforeDisable(), 插件开发者可在此回收资源
+ var pluginDisableResult = plugin.BeforeDisable();
+ if (!pluginDisableResult.IsSuccess)
+ {
+ throw Oops.Oh("禁用失败: 来自插件的错误信息: " + pluginDisableResult.Message);
+
+ }
+ // 3.移除插件对应的程序集加载上下文
+ _pluginManager.UnloadPlugin(pluginId);
+ // 3.1. ReBuild
+ this._pluginApplicationBuilderManager.ReBuild();
+ if (pluginConfigModel.EnabledPlugins.Contains(pluginId))
+ {
+ // 4.从 pluginConfigModel.EnabledPlugins 移除
+ pluginConfigModel.EnabledPlugins.Remove(pluginId);
+ // 5.保存到 plugin.config.json
+ PluginConfigModelFactory.Save(pluginConfigModel);
+ }
+ }
+ catch (Exception ex)
+ {
+ PluginCore.Utils.LogUtil.Error(ex.ToString());
+ throw Oops.Oh("禁用失败: 此插件不存在, 或未启用");
+
+ }
+
+ // 7. 尝试移除 Plugins_wwwroot/PluginId
+ string pluginWwwRootDir = PluginPathProvider.PluginWwwRootDir(pluginId);
+ if (Directory.Exists(pluginWwwRootDir))
+ {
+ Directory.Delete(pluginWwwRootDir, true);
+ }
+ //8.移除Furion动态插件
+
+ //9.载入Furion动态插件
+ var pluginMainAssembly = _pluginManager.GetPluginAssembly(pluginId);
+ // 将程序集添加进动态 WebAPI 应用部件
+ _provider.RemoveAssembliesWithNotifyChanges(pluginMainAssembly);
+
+
+ await _sysPluginRep.UpdateAsync(u => new SysPluginCore() { Status = StatusEnum.Disable }, u => u.Id == input.Id);
+ }
+ catch (Exception ex)
+ {
+ throw Oops.Oh("禁用失败: " + ex.Message);
+
+ }
+
+
+ }
+ ///
+ /// 上传文件
+ ///
+ ///
+ ///
+ ///
+ [DisplayName("上传文件")]
+ public async Task UploadFile([Required] IFormFile file, [FromQuery] string? path)
+ {
+ var sysFile = await HandleUploadFile(file, path);
+ //return new FileOutput
+ //{
+
+ //};
+ }
+
+ ///
+ /// 上传文件
+ ///
+ /// 文件
+ /// 路径
+ ///
+ [NonAction]
+ private async Task HandleUploadFile(IFormFile file, string savePath)
+ {
+ if (file == null) throw Oops.Oh(ErrorCodeEnum.D8000);
+
+ var path = savePath;
+
+ BaseResponseModel responseData = new BaseResponseModel();
+
+ #region 效验
+ if (file == null)
+ {
+ throw Oops.Oh("上传的文件不能为空");
+ //responseData.Code = -1;
+ //responseData.Message = "上传的文件不能为空";
+ //return responseData;
+ }
+ //文件后缀
+ string fileExtension = Path.GetExtension(file.FileName);//获取文件格式,拓展名
+ // 类型标记
+ UploadFileType uploadFileType = UploadFileType.NoAllowedType;
+ switch (fileExtension)
+ {
+ case ".zip":
+ uploadFileType = UploadFileType.Zip;
+ break;
+ case ".nupkg":
+ uploadFileType = UploadFileType.Nupkg;
+ break;
+ }
+
+ if (fileExtension != ".zip" && fileExtension != ".nupkg")
+ {
+ throw Oops.Oh("只能上传 zip 或 nupkg 格式文件");
+ //responseData.Code = -1;
+ //// nupkg 其实就是 zip
+ //responseData.Message = "只能上传 zip 或 nupkg 格式文件";
+ //return responseData;
+ }
+ // PluginCore.AspNetCore-v1.0.2 起 不再限制插件上传大小
+ //判断文件大小
+ //var fileSize = file.Length;
+ //if (fileSize > 1024 * 1024 * 5) // 5M
+ //{
+ // responseData.Code = -1;
+ // responseData.Message = "上传的文件不能大于5MB";
+ // return responseData;
+ //}
+ #endregion
+
+ try
+ {
+ // 1.先上传到 临时插件上传目录, 用Guid.zip作为保存文件名
+ string tempZipFilePath = Path.Combine(PluginPathProvider.TempPluginUploadDir(), Guid.NewGuid() + ".zip");
+ using (var fs = System.IO.File.Create(tempZipFilePath))
+ {
+ file.CopyTo(fs); //将上传的文件文件流,复制到fs中
+ fs.Flush();//清空文件流
+ }
+ // 2.解压
+ bool isDecomparessSuccess = false;
+ if (uploadFileType == UploadFileType.Zip)
+ {
+ isDecomparessSuccess = PluginCore.Utils.ZipHelper.DecomparessFile(tempZipFilePath, tempZipFilePath.Replace(".zip", ""));
+ }
+ else if (uploadFileType == UploadFileType.Nupkg)
+ {
+ isDecomparessSuccess = NupkgService.DecomparessFile(tempZipFilePath, tempZipFilePath.Replace(".zip", ""));
+ }
+
+ // 3.删除原压缩包
+ System.IO.File.Delete(tempZipFilePath);
+ if (!isDecomparessSuccess)
+ {
+ throw Oops.Oh("解压插件压缩包失败");
+
+ //responseData.Code = -1;
+ //responseData.Message = "解压插件压缩包失败";
+ //return responseData;
+ }
+ // 4.读取其中的info.json, 获取 PluginId 值
+ PluginInfoModel pluginInfoModel = PluginInfoModelFactory.ReadPluginDir(tempZipFilePath.Replace(".zip", ""));
+ if (pluginInfoModel == null || string.IsNullOrEmpty(pluginInfoModel.PluginId))
+ {
+ // 记得删除已不再需要的临时插件文件夹
+ Directory.Delete(tempZipFilePath.Replace(".zip", ""), true);
+ throw Oops.Oh("不合法的插件");
+
+ //responseData.Code = -1;
+ //responseData.Message = "不合法的插件";
+ //return responseData;
+ }
+ string pluginId = pluginInfoModel.PluginId;
+ // 5.检索 此 PluginId 是否本地插件已存在
+ var pluginConfigModel = PluginConfigModelFactory.Create();
+ // 本地已经存在的 PluginId
+ IList localExistPluginIds = PluginPathProvider.AllPluginFolderName();
+ if (localExistPluginIds.Contains(pluginId))
+ {
+ // 记得删除已不再需要的临时插件文件夹
+ Directory.Delete(tempZipFilePath.Replace(".zip", ""), true);
+ throw Oops.Oh($"本地已有此插件 (PluginId: {pluginId}), 请前往插件列表删除后, 再上传");
+
+ //responseData.Code = -1;
+ //responseData.Message = $"本地已有此插件 (PluginId: {pluginId}), 请前往插件列表删除后, 再上传";
+ //return responseData;
+ }
+ // 6.本地无此插件 -> 移动插件文件夹到 Plugins 下, 并以 PluginId 为插件文件夹名
+ string pluginsRootPath = PluginPathProvider.PluginsRootPath();
+ string newPluginDir = Path.Combine(pluginsRootPath, pluginId);
+ Directory.Move(tempZipFilePath.Replace(".zip", ""), newPluginDir);
+
+ // 7. 放入 Plugins 中, 默认为 已禁用
+
+ var pluginCore = new SysPluginCore();
+ pluginCore.PluginId = pluginId;
+ pluginCore.DisplayName = pluginInfoModel.DisplayName;
+ pluginCore.Description = pluginInfoModel.Description;
+ pluginCore.Author = pluginInfoModel.Author;
+ pluginCore.Version = pluginInfoModel.Version;
+ pluginCore.Status = StatusEnum.Disable;
+ await _sysPluginRep.InsertAsync(pluginCore.Adapt());
+
+
+ responseData.Code = 1;
+ responseData.Message = $"上传插件成功 (PluginId: {pluginId})";
+ }
+ catch (Exception ex)
+ {
+ throw Oops.Oh("上传插件失败: " + ex.Message);
+
+ //responseData.Code = -1;
+ //responseData.Message = "上传插件失败: " + ex.Message;
+ //ex = ex.InnerException;
+ //while (ex != null)
+ //{
+ // responseData.Message += " - " + ex.InnerException.Message;
+ // ex = ex.InnerException;
+ //}
+ }
+
+
+
+ return responseData;
+ }
+
+
+ #region Helpers
+
+ [NonAction]
+ private IList PluginInfoModelToResponseModel(IList pluginInfoModels, PluginConfigModel pluginConfigModel, string[] enablePluginIds)
+ {
+ // 获取 Plugins 下所有插件
+ // DirectoryInfo pluginsDir = new DirectoryInfo(PluginPathProvider.PluginsRootPath());
+ // List pluginIds = pluginsDir?.GetDirectories()?.Select(m => m.Name)?.ToList() ?? new List();
+
+ IList responseModels = new List();
+ #region 添加插件状态信息
+ foreach (var model in pluginInfoModels)
+ {
+ PluginInfoResponseModel responseModel = new PluginInfoResponseModel();
+ responseModel.Author = model.Author;
+ responseModel.Description = model.Description;
+ responseModel.DisplayName = model.DisplayName;
+ responseModel.PluginId = model.PluginId;
+ responseModel.SupportedVersions = model.SupportedVersions;
+ responseModel.Version = model.Version;
+ responseModel.DependPlugins = model.DependPlugins;
+
+ if (pluginConfigModel.EnabledPlugins.Contains(model.PluginId) && !enablePluginIds.Contains(model.PluginId))
+ {
+ // 错误情况: 配置 标识 已启用, 但实际没有启用成功
+ pluginConfigModel.EnabledPlugins.Remove(model.PluginId);
+ PluginConfigModelFactory.Save(pluginConfigModel);
+
+ responseModel.Status = PluginStatus.Disabled;
+ }
+ else if (!pluginConfigModel.EnabledPlugins.Contains(model.PluginId) && enablePluginIds.Contains(model.PluginId))
+ {
+ // 错误情况: 配置没有标识 已启用, 但实际 已启用
+ pluginConfigModel.EnabledPlugins.Add(model.PluginId);
+ PluginConfigModelFactory.Save(pluginConfigModel);
+
+ responseModel.Status = PluginStatus.Enabled;
+ }
+ else if (pluginConfigModel.EnabledPlugins.Contains(model.PluginId) && enablePluginIds.Contains(model.PluginId))
+ {
+ responseModel.Status = PluginStatus.Enabled;
+ }
+ else
+ {
+ responseModel.Status = PluginStatus.Disabled;
+ }
+ responseModels.Add(responseModel);
+ }
+ #endregion
+
+ return responseModels;
+ }
+
+ public enum UploadFileType
+ {
+ NoAllowedType = 0,
+ Zip = 1,
+ Nupkg = 2
+ }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Startup.cs b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Startup.cs
new file mode 100644
index 00000000..f62088d5
--- /dev/null
+++ b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/Startup.cs
@@ -0,0 +1,34 @@
+// 麻省理工学院许可证
+//
+// 版权所有 (c) 2021-2023 zuohuaijun,大名科技(天津)有限公司 联系电话/微信:18020030720 QQ:515096995
+//
+// 特此免费授予获得本软件的任何人以处理本软件的权利,但须遵守以下条件:在所有副本或重要部分的软件中必须包括上述版权声明和本许可声明。
+//
+// 软件按“原样”提供,不提供任何形式的明示或暗示的保证,包括但不限于对适销性、适用性和非侵权的保证。
+// 在任何情况下,作者或版权持有人均不对任何索赔、损害或其他责任负责,无论是因合同、侵权或其他方式引起的,与软件或其使用或其他交易有关。
+
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.DependencyInjection;
+using PluginCore.AspNetCore.Extensions;
+using System;
+
+namespace Admin.NET.Plugin.PluginCoreManager;
+
+[AppStartup(100)]
+public class Startup : AppStartup
+{
+ public void ConfigureServices(IServiceCollection services)
+ {
+ RunOptions.Default
+ .AddComponent()
+ .UseComponent();
+
+
+ }
+
+ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
+ {
+
+ }
+}
\ No newline at end of file
diff --git a/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/StartupServiceComponent.cs b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/StartupServiceComponent.cs
new file mode 100644
index 00000000..a0ada15a
--- /dev/null
+++ b/Admin.NET/Plugins/Admin.NET.Plugin.PluginCoreManager/StartupServiceComponent.cs
@@ -0,0 +1,56 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.DependencyInjection;
+using PluginCore;
+using PluginCore.AspNetCore.Extensions;
+using PluginCore.Interfaces;
+using PluginCore.Models;
+using static SKIT.FlurlHttpClient.Wechat.Api.Models.WxaPluginListResponse.Types;
+
+namespace Admin.NET.Plugin.PluginCoreManager;
+
+// 模拟 ConfigureService
+public sealed class StartupServiceComponent : IServiceComponent
+{
+ public void Load(IServiceCollection services, ComponentContext componentContext)
+ {
+ // 1. 添加 PluginCore
+ services.AddPluginCore();
+ IPluginManager pluginManager = App.GetService();
+ IDynamicApiRuntimeChangeProvider provider = App.GetService();
+ #region 获取 PluginConfigModel
+ PluginConfigModel pluginConfigModel = PluginConfigModelFactory.Create();
+ #endregion
+
+ // 已启用的插件
+ #region 加载 已启用插件的Assemblies
+ IList enabledPluginIds = pluginConfigModel.EnabledPlugins;
+ foreach (var pluginId in enabledPluginIds)
+ {
+ //9.载入Furion动态插件
+ var pluginMainAssembly = pluginManager.GetPluginAssembly(pluginId);
+ // 将程序集添加进动态 WebAPI 应用部件
+ provider.AddAssembliesWithNotifyChanges(pluginMainAssembly);
+ }
+ #endregion
+
+
+
+ }
+}
+
+// 模拟 Configure
+public sealed class StartupApplicationComponent : IApplicationComponent
+{
+ public void Load(IApplicationBuilder app, IWebHostEnvironment env, ComponentContext componentContext)
+ {
+ // 2. 使用 PluginCore
+ app.UsePluginCore();
+ }
+}
diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/AdminUI/PluginCoreAdminUIBuilderExtensions.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/AdminUI/PluginCoreAdminUIBuilderExtensions.cs
new file mode 100644
index 00000000..af9e0f8e
--- /dev/null
+++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/AdminUI/PluginCoreAdminUIBuilderExtensions.cs
@@ -0,0 +1,95 @@
+//===================================================
+// License: GNU LGPLv3
+// Contributors: yiyungent@gmail.com
+// Project: https://yiyungent.github.io/PluginCore
+// GitHub: https://github.com/yiyungent/PluginCore
+//===================================================
+
+
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.FileProviders;
+using Microsoft.Extensions.Options;
+
+namespace PluginCore.AspNetCore.AdminUI
+{
+ public static class PluginCoreAdminUIBuilderExtensions
+ {
+
+ ///
+ /// Register the SwaggerUI middleware with provided options
+ ///
+ public static IApplicationBuilder UsePluginCoreAdminUI(this IApplicationBuilder app, PluginCoreAdminUIOptions options)
+ {
+ #region Old - 区分
+ //Config.PluginCoreConfig pluginCoreConfig = Config.PluginCoreConfigFactory.Create();
+
+ //switch (pluginCoreConfig.FrontendMode?.ToLower())
+ //{
+ // case "localembedded":
+ // app.UseMiddleware(options);
+ // break;
+ // case "localfolder":
+
+ // #region LocalFolder
+ // //string contentRootPath = Directory.GetCurrentDirectory();
+
+ // // https://docs.microsoft.com/zh-CN/aspnet/core/fundamentals/static-files?view=aspnetcore-5.0
+ // //var options = new DefaultFilesOptions()
+ // //{
+ // // RequestPath = "/PluginCore/Admin",
+ // //};
+ // //// TODO: 404: 无效, 失败, 改为使用 Controller 手动指定
+ // ////options.DefaultFileNames.Add("PluginCoreAdmin/index.html");
+ // //app.UseDefaultFiles(options);
+
+ // // 注意: 为了无需重启Web,而更新是否本地前端配置, 因此此项保持常驻开启
+ // // 因此, 需要保证 PluginCoreAdmin 文件夹存在
+ // string pluginCoreAdminDir = PluginPathProvider.PluginCoreAdminDir();
+ // app.UseStaticFiles(new StaticFileOptions
+ // {
+ // FileProvider = new PhysicalFileProvider(
+ // pluginCoreAdminDir),
+ // RequestPath = "/PluginCore/Admin"
+ // });
+ // #endregion
+
+ // break;
+ // case "remotecdn":
+
+ // break;
+ // default:
+ // app.UseMiddleware(options);
+ // break;
+ //}
+
+ //return app;
+ #endregion
+
+
+ return app.UseMiddleware(options);
+ }
+
+ ///
+ /// Register the SwaggerUI middleware with optional setup action for DI-injected options
+ ///
+ public static IApplicationBuilder UsePluginCoreAdminUI(
+ this IApplicationBuilder app)
+ {
+ PluginCoreAdminUIOptions options = new PluginCoreAdminUIOptions()
+ {
+
+ };
+
+ return app.UsePluginCoreAdminUI(options);
+ }
+
+
+ }
+}
+
+
diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/AdminUI/PluginCoreAdminUIMiddleware.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/AdminUI/PluginCoreAdminUIMiddleware.cs
new file mode 100644
index 00000000..b0890d39
--- /dev/null
+++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/AdminUI/PluginCoreAdminUIMiddleware.cs
@@ -0,0 +1,150 @@
+//===================================================
+// License: GNU LGPLv3
+// Contributors: yiyungent@gmail.com
+// Project: https://yiyungent.github.io/PluginCore
+// GitHub: https://github.com/yiyungent/PluginCore
+//===================================================
+
+
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.StaticFiles;
+using Microsoft.Extensions.FileProviders;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace PluginCore.AspNetCore.AdminUI
+{
+ public class PluginCoreAdminUIMiddleware
+ {
+ private const string EmbeddedFileNamespace = "PluginCore.AspNetCore.node_modules.plugincore_admin_frontend.dist";
+
+ private readonly PluginCoreAdminUIOptions _options;
+ private readonly StaticFileMiddleware _staticFileMiddleware;
+
+
+ public PluginCoreAdminUIMiddleware(
+ RequestDelegate next,
+ IWebHostEnvironment hostingEnv,
+ ILoggerFactory loggerFactory,
+ PluginCoreAdminUIOptions options)
+ {
+ _options = options ?? new PluginCoreAdminUIOptions();
+
+ _staticFileMiddleware = CreateStaticFileMiddleware(next, hostingEnv, loggerFactory, options);
+ }
+
+ public async Task Invoke(HttpContext httpContext)
+ {
+ var httpMethod = httpContext.Request.Method;
+ var path = httpContext.Request.Path.Value;
+
+ // If the RoutePrefix is requested (with or without trailing slash), redirect to index URL
+ if (httpMethod == "GET" && Regex.IsMatch(path, $"^/?{Regex.Escape(_options.RoutePrefix)}/?$", RegexOptions.IgnoreCase))
+ {
+ // Use relative redirect to support proxy environments
+ var relativeIndexUrl = string.IsNullOrEmpty(path) || path.EndsWith("/")
+ ? "index.html"
+ : $"{path.Split('/').Last()}/index.html";
+
+ RespondWithRedirect(httpContext.Response, relativeIndexUrl);
+ return;
+ }
+
+ if (httpMethod == "GET" && Regex.IsMatch(path, $"^/{Regex.Escape(_options.RoutePrefix)}/?index.html$", RegexOptions.IgnoreCase))
+ {
+ await RespondWithIndexHtml(httpContext.Response);
+ return;
+ }
+
+ await _staticFileMiddleware.Invoke(httpContext);
+ }
+
+ private StaticFileMiddleware CreateStaticFileMiddleware(
+ RequestDelegate next,
+ IWebHostEnvironment hostingEnv,
+ ILoggerFactory loggerFactory,
+ PluginCoreAdminUIOptions options)
+ {
+ Config.PluginCoreConfig pluginCoreConfig = Config.PluginCoreConfigFactory.Create();
+ IFileProvider fileProvider = null;
+ switch (pluginCoreConfig.FrontendMode?.ToLower())
+ {
+ case "localembedded":
+ fileProvider = new EmbeddedFileProvider(typeof(PluginCoreAdminUIMiddleware).GetTypeInfo().Assembly,
+ EmbeddedFileNamespace);
+ break;
+ case "localfolder":
+ string absoluteRootPath = PluginPathProvider.PluginCoreAdminDir();
+ fileProvider = new PhysicalFileProvider(absoluteRootPath);
+ break;
+ case "remotecdn":
+ fileProvider = new PluginCoreAdminUIRemoteFileProvider(pluginCoreConfig.RemoteFrontend);
+ break;
+ default:
+ fileProvider = new EmbeddedFileProvider(typeof(PluginCoreAdminUIMiddleware).GetTypeInfo().Assembly,
+ EmbeddedFileNamespace);
+ break;
+ }
+
+ var staticFileOptions = new StaticFileOptions
+ {
+ RequestPath = string.IsNullOrEmpty(options.RoutePrefix) ? string.Empty : $"/{options.RoutePrefix}",
+ FileProvider = fileProvider,
+ };
+
+ return new StaticFileMiddleware(next, hostingEnv, Options.Create(staticFileOptions), loggerFactory);
+ }
+
+ private void RespondWithRedirect(HttpResponse response, string location)
+ {
+ response.StatusCode = 301;
+ response.Headers["Location"] = location;
+ }
+
+ private async Task RespondWithIndexHtml(HttpResponse response)
+ {
+ response.StatusCode = 200;
+ response.ContentType = "text/html;charset=utf-8";
+
+ using (var stream = _options.IndexStream())
+ {
+ // Inject arguments before writing to response
+ var htmlBuilder = new StringBuilder(new StreamReader(stream).ReadToEnd());
+ foreach (var entry in GetIndexArguments())
+ {
+ htmlBuilder.Replace(entry.Key, entry.Value);
+ }
+
+ await response.WriteAsync(htmlBuilder.ToString(), Encoding.UTF8);
+ }
+ }
+
+ private IDictionary GetIndexArguments()
+ {
+ return new Dictionary()
+ {
+ //{ "%(DocumentTitle)", _options.DocumentTitle },
+ //{ "%(HeadContent)", _options.HeadContent },
+ //{ "%(ConfigObject)", JsonSerializer.Serialize(_options.ConfigObject, _jsonSerializerOptions) },
+ //{ "%(OAuthConfigObject)", JsonSerializer.Serialize(_options.OAuthConfigObject, _jsonSerializerOptions) },
+ //{ "%(Interceptors)", JsonSerializer.Serialize(_options.Interceptors) },
+ };
+ }
+
+ }
+}
+
+
diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/AdminUI/PluginCoreAdminUIOptions.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/AdminUI/PluginCoreAdminUIOptions.cs
new file mode 100644
index 00000000..9a004237
--- /dev/null
+++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/AdminUI/PluginCoreAdminUIOptions.cs
@@ -0,0 +1,68 @@
+//===================================================
+// License: GNU LGPLv3
+// Contributors: yiyungent@gmail.com
+// Project: https://yiyungent.github.io/PluginCore
+// GitHub: https://github.com/yiyungent/PluginCore
+//===================================================
+
+
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net.Http;
+using System.Reflection;
+using System.Text;
+
+namespace PluginCore.AspNetCore.AdminUI
+{
+ public class PluginCoreAdminUIOptions
+ {
+ ///
+ /// Gets or sets a route prefix for accessing the swagger-ui
+ ///
+ public string RoutePrefix { get; set; } = "PluginCore/Admin";
+
+ ///
+ /// Gets or sets a Stream function for retrieving the swagger-ui page
+ ///
+ public Func IndexStream
+ {
+ get
+ {
+ Func funcStream = null;
+ ;
+ Config.PluginCoreConfig pluginCoreConfig = Config.PluginCoreConfigFactory.Create();
+ switch (pluginCoreConfig.FrontendMode?.ToLower())
+ {
+ case "localembedded":
+ funcStream = () => typeof(PluginCoreAdminUIOptions).GetTypeInfo().Assembly
+ .GetManifestResourceStream("PluginCore.AspNetCore.node_modules.plugincore_admin_frontend.dist.index.html");
+ break;
+ case "localfolder":
+ string absoluteRootPath = PluginPathProvider.PluginCoreAdminDir();
+ string indexFilePath = Path.Combine(absoluteRootPath, "index.html");
+
+ funcStream = () => (Stream)new FileStream(indexFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, 1, FileOptions.Asynchronous | FileOptions.SequentialScan);
+ break;
+ case "remotecdn":
+ string remoteFrontendRootPath = pluginCoreConfig.RemoteFrontend;
+ string indexFileRemotePath = remoteFrontendRootPath + "/" + "index.html";
+
+ funcStream = () => new HttpClient().GetStreamAsync(indexFileRemotePath).Result;
+ break;
+ default:
+ funcStream = () => typeof(PluginCoreAdminUIOptions).GetTypeInfo().Assembly
+ .GetManifestResourceStream("PluginCore.AspNetCore.node_modules.plugincore_admin_frontend.dist.index.html");
+ break;
+ }
+
+ return funcStream;
+ }
+ }
+
+
+ }
+}
+
+
diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/AdminUI/PluginCoreAdminUIRemoteFileProvider.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/AdminUI/PluginCoreAdminUIRemoteFileProvider.cs
new file mode 100644
index 00000000..1e134dc1
--- /dev/null
+++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/AdminUI/PluginCoreAdminUIRemoteFileProvider.cs
@@ -0,0 +1,132 @@
+//===================================================
+// License: GNU LGPLv3
+// Contributors: yiyungent@gmail.com
+// Project: https://yiyungent.github.io/PluginCore
+// GitHub: https://github.com/yiyungent/PluginCore
+//===================================================
+
+
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net.Http;
+using System.Text;
+using Microsoft.Extensions.FileProviders;
+using Microsoft.Extensions.Primitives;
+
+namespace PluginCore.AspNetCore.AdminUI
+{
+ public class PluginCoreAdminUIRemoteFileProvider : IFileProvider
+ {
+ protected string RootUrl { get; set; }
+
+ public PluginCoreAdminUIRemoteFileProvider(string rootUrl)
+ {
+ this.RootUrl = rootUrl;
+ }
+
+ public IDirectoryContents GetDirectoryContents(string subpath)
+ {
+ return (IDirectoryContents)NotFoundDirectoryContents.Singleton;
+ }
+
+ public IFileInfo GetFileInfo(string subpath)
+ {
+ if (string.IsNullOrEmpty(subpath))
+ return (IFileInfo)new NotFoundFileInfo(subpath);
+
+ IFileInfo fileInfo = new PluginCoreAdminUIFileInfo(this.RootUrl, subpath);
+
+ return fileInfo;
+ }
+
+ public IChangeToken Watch(string filter)
+ {
+ throw new NotImplementedException();
+ }
+
+
+ public class PluginCoreAdminUIFileInfo : IFileInfo
+ {
+ protected string RootUrl { get; set; }
+
+ protected string SubPath { get; set; }
+
+ private string _name;
+
+ public PluginCoreAdminUIFileInfo(string rootUrl, string subpath)
+ {
+ this.RootUrl = rootUrl;
+ this.SubPath = subpath;
+ this._name = this.SubPath.Substring(this.SubPath.LastIndexOf("/") + 1);
+ }
+
+ public Stream CreateReadStream()
+ {
+ HttpClient httpClient = new HttpClient();
+
+ return httpClient.GetStreamAsync($"{this.RootUrl}/{this.SubPath}").Result;
+ }
+
+ public bool Exists
+ {
+ get
+ {
+ bool isExist = false;
+ if (this.Name == "index.html")
+ {
+ isExist = true;
+ }
+
+ return isExist;
+ }
+ }
+ public bool IsDirectory
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ public DateTimeOffset LastModified
+ {
+ get
+ {
+ return new DateTimeOffset(DateTime.Now);
+ }
+ }
+
+ public long Length
+ {
+ get
+ {
+ return 111;
+ }
+ }
+
+ public string Name
+ {
+ get
+ {
+ return this._name;
+ }
+ }
+
+ public string PhysicalPath
+ {
+ get
+ {
+ return "";
+ }
+ }
+ }
+
+ }
+
+
+
+}
+
+
diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authentication/PluginCoreAuthenticationHandler.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authentication/PluginCoreAuthenticationHandler.cs
new file mode 100644
index 00000000..bd9ab972
--- /dev/null
+++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authentication/PluginCoreAuthenticationHandler.cs
@@ -0,0 +1,72 @@
+//===================================================
+// License: GNU LGPLv3
+// Contributors: yiyungent@gmail.com
+// Project: https://yiyungent.github.io/PluginCore
+// GitHub: https://github.com/yiyungent/PluginCore
+//===================================================
+
+
+
+using System;
+using System.Collections.Generic;
+using System.Security.Claims;
+using System.Text;
+using System.Text.Encodings.Web;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using PluginCore.AspNetCore.Authorization;
+
+namespace PluginCore.AspNetCore.Authentication
+{
+ ///
+ /// https://stackoverflow.com/questions/52287542/invalidoperationexception-no-authenticationscheme-was-specified-and-there-was
+ ///
+ public class PluginCoreAuthenticationHandler : AuthenticationHandler
+ {
+ private readonly AccountManager _accountManager;
+
+ public PluginCoreAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock, AccountManager accountManager) : base(options, logger, encoder, clock)
+ {
+ this._accountManager = accountManager;
+ }
+
+ protected override async Task HandleAuthenticateAsync()
+ {
+ string token = this._accountManager.CurrentToken();
+ if (string.IsNullOrEmpty(token))
+ {
+ return AuthenticateResult.NoResult();
+ }
+
+ bool isAdmin = AccountManager.IsAdminToken(token);
+
+ if (!isAdmin)
+ {
+ return AuthenticateResult.Fail($"token is not admin");
+ }
+ else
+ {
+ var id = new ClaimsIdentity(
+ // new Claim[] { new Claim("PluginCore.Token", token) }, // not safe , just as an example , should custom claims on your own
+ claims: new Claim[] { new Claim(type: IPlugins.Constants.AspNetCoreAuthenticationClaimType, value: token) }, // not safe , just as an example , should custom claims on your own
+ authenticationType: Scheme.Name
+ );
+ ClaimsPrincipal principal = new ClaimsPrincipal(identity: id);
+ var ticket = new AuthenticationTicket(
+ principal: principal,
+ properties: new AuthenticationProperties(),
+ authenticationScheme: Scheme.Name);
+
+ // Utils.LogUtil.Info($"通过 Authentication: token: {token}");
+ Utils.LogUtil.Info($"Authentication Passed");
+
+ return AuthenticateResult.Success(ticket);
+ }
+
+ }
+ }
+}
+
+
diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authentication/PluginCoreAuthenticationSchemeOptions.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authentication/PluginCoreAuthenticationSchemeOptions.cs
new file mode 100644
index 00000000..53fd10a6
--- /dev/null
+++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authentication/PluginCoreAuthenticationSchemeOptions.cs
@@ -0,0 +1,23 @@
+//===================================================
+// License: GNU LGPLv3
+// Contributors: yiyungent@gmail.com
+// Project: https://yiyungent.github.io/PluginCore
+// GitHub: https://github.com/yiyungent/PluginCore
+//===================================================
+
+
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Microsoft.AspNetCore.Authentication;
+
+namespace PluginCore.AspNetCore.Authentication
+{
+ public class PluginCoreAuthenticationSchemeOptions : AuthenticationSchemeOptions
+ {
+
+ }
+}
+
+
diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authorization/AccountManager.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authorization/AccountManager.cs
new file mode 100644
index 00000000..5b3f9870
--- /dev/null
+++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authorization/AccountManager.cs
@@ -0,0 +1,149 @@
+//===================================================
+// License: GNU LGPLv3
+// Contributors: yiyungent@gmail.com
+// Project: https://yiyungent.github.io/PluginCore
+// GitHub: https://github.com/yiyungent/PluginCore
+//===================================================
+
+
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Microsoft.AspNetCore.Http;
+using PluginCore.Utils;
+
+namespace PluginCore.AspNetCore.Authorization
+{
+ public class AccountManager
+ {
+ private readonly IHttpContextAccessor _httpContextAccessor;
+
+ public Microsoft.AspNetCore.Http.HttpContext HttpContext
+ {
+ get
+ {
+ return this._httpContextAccessor.HttpContext;
+ }
+ }
+
+ public AccountManager(IHttpContextAccessor httpContextAccessor)
+ {
+ // Exception: IFeatureCollection has been disposed. Object name: 'Collection'.
+ // https://stackoverflow.com/questions/59963383/session-setstring-method-throws-exception-ifeaturecollection-has-been-disposed
+ //HttpContext = ((HttpContextAccessor)httpContextAccessor).HttpContext;
+ //HttpContext = httpContextAccessor.HttpContext;
+ // 注意: 不要将 HttpContext 保存起来,应当每次都从 httpContextAccessor 取
+ _httpContextAccessor = httpContextAccessor;
+ }
+
+ public static Config.PluginCoreConfig.AdminModel Admin
+ {
+ get
+ {
+ return Config.PluginCoreConfigFactory.Create().Admin;
+ }
+ set
+ {
+ var sourceModel = Config.PluginCoreConfigFactory.Create();
+ sourceModel.Admin = value;
+ Config.PluginCoreConfigFactory.Save(sourceModel);
+ }
+ }
+
+ public static string CurrentToken(HttpContext httpContext)
+ {
+ string token = null;
+ HttpRequest request = httpContext.Request;
+ try
+ {
+ // header -> cookie
+ try
+ {
+ // header 中找 token
+ if (request.Headers.ContainsKey("Authorization"))
+ {
+ string authHeader = request.Headers["Authorization"];
+ if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Bearer"))
+ {
+ token = authHeader.Substring("Bearer ".Length).Trim();
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ //throw ex;
+ }
+ if (string.IsNullOrEmpty(token))
+ {
+ // cookie 中找 token
+ //string tokenCookieName = "token";
+ // string tokenCookieName = "PluginCore.Admin.Token";
+ string tokenCookieName = IPlugins.Constants.AspNetCoreAuthorizationTokenCookieName;
+ if (request.Cookies.Keys.Contains(tokenCookieName))
+ {
+ if (request.Cookies[tokenCookieName] != null && string.IsNullOrEmpty(request.Cookies[tokenCookieName]) == false)
+ {
+ token = request.Cookies[tokenCookieName];
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ throw ex;
+ }
+
+ return token;
+ }
+
+ public string CurrentToken()
+ {
+ return CurrentToken(this.HttpContext);
+ }
+
+ public static string CreateToken()
+ {
+ return CreateToken(Admin.UserName, Admin.Password);
+ }
+
+ public static string CreateToken(string userName, string password)
+ {
+ string token = $"UserName={userName}&Password={password}";
+ token = Md5Helper.MD5Encrypt32(token);
+
+ return token;
+ }
+
+ public static bool IsAdminToken(string token)
+ {
+ bool isAdmin = false;
+ isAdmin = CreateToken().Equals(token);
+
+ return isAdmin;
+ }
+
+ public bool IsAdmin()
+ {
+ return IsAdmin(this.HttpContext);
+ }
+
+ public static bool IsAdmin(HttpContext httpContext)
+ {
+ bool isAdmin = false;
+ try
+ {
+ string currentToken = CurrentToken(httpContext);
+ isAdmin = IsAdminToken(currentToken);
+ }
+ catch (Exception ex)
+ {
+ throw ex;
+ }
+
+ return isAdmin;
+ }
+ }
+}
+
+
diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authorization/PluginCoreAdminAuthorizationHandler.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authorization/PluginCoreAdminAuthorizationHandler.cs
new file mode 100644
index 00000000..215cef21
--- /dev/null
+++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authorization/PluginCoreAdminAuthorizationHandler.cs
@@ -0,0 +1,59 @@
+//===================================================
+// License: GNU LGPLv3
+// Contributors: yiyungent@gmail.com
+// Project: https://yiyungent.github.io/PluginCore
+// GitHub: https://github.com/yiyungent/PluginCore
+//===================================================
+
+
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization;
+
+namespace PluginCore.AspNetCore.Authorization
+{
+ public class PluginCoreAdminAuthorizationHandler : AuthorizationHandler
+ {
+ private readonly AccountManager _accountManager;
+
+ public PluginCoreAdminAuthorizationHandler(AccountManager accountManager)
+ {
+ _accountManager = accountManager;
+ }
+
+ ///
+ /// 必须在其中呼叫一次 代表满足 ,否则皆为 不满足此 Requirement
+ ///
+ ///
+ ///
+ ///
+ protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
+ PluginCoreAdminRequirement requirement)
+ {
+ bool isAdmin = this._accountManager.IsAdmin();
+ if (!isAdmin)
+ {
+ context.Fail();
+ }
+ else
+ {
+ // 认证通过后, 可通过下面方式获取 token
+ var identity = context.User.Identity;
+
+ string token = this._accountManager.CurrentToken();
+
+ // Utils.LogUtil.Info($"通过 Authorization: token: {token}");
+ Utils.LogUtil.Info($"Authorization Granted");
+
+ context.Succeed(requirement);
+ }
+
+ await Task.CompletedTask;
+ }
+ }
+}
+
+
diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authorization/PluginCoreAdminAuthorizeAttribute.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authorization/PluginCoreAdminAuthorizeAttribute.cs
new file mode 100644
index 00000000..dc85f768
--- /dev/null
+++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authorization/PluginCoreAdminAuthorizeAttribute.cs
@@ -0,0 +1,31 @@
+//===================================================
+// License: GNU LGPLv3
+// Contributors: yiyungent@gmail.com
+// Project: https://yiyungent.github.io/PluginCore
+// GitHub: https://github.com/yiyungent/PluginCore
+//===================================================
+
+
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Microsoft.AspNetCore.Authorization;
+
+namespace PluginCore.AspNetCore.Authorization
+{
+ ///
+ /// 注意: PluginCoreAdmin -> PluginCore.Admin
+ ///
+ public class PluginCoreAdminAuthorizeAttribute : AuthorizeAttribute
+ {
+ // public PluginCoreAdminAuthorizeAttribute() : base("PluginCore.Admin")
+ public PluginCoreAdminAuthorizeAttribute() : base(policy: IPlugins.Constants.AspNetCoreAuthorizationPolicyName)
+ {
+ // 同时明确指定 认证方案 与 授权策略
+ AuthenticationSchemes = PluginCore.IPlugins.Constants.AspNetCoreAuthenticationScheme;
+ }
+ }
+}
+
+
diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authorization/PluginCoreAdminRequirement.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authorization/PluginCoreAdminRequirement.cs
new file mode 100644
index 00000000..5ee934bc
--- /dev/null
+++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Authorization/PluginCoreAdminRequirement.cs
@@ -0,0 +1,26 @@
+//===================================================
+// License: GNU LGPLv3
+// Contributors: yiyungent@gmail.com
+// Project: https://yiyungent.github.io/PluginCore
+// GitHub: https://github.com/yiyungent/PluginCore
+//===================================================
+
+
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Microsoft.AspNetCore.Authorization;
+
+namespace PluginCore.AspNetCore.Authorization
+{
+ public class PluginCoreAdminRequirement : IAuthorizationRequirement
+ {
+ public PluginCoreAdminRequirement()
+ {
+
+ }
+ }
+}
+
+
diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/BackgroundServices/BackgroundServicesHelper.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/BackgroundServices/BackgroundServicesHelper.cs
new file mode 100644
index 00000000..62644fd4
--- /dev/null
+++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/BackgroundServices/BackgroundServicesHelper.cs
@@ -0,0 +1,27 @@
+//===================================================
+// License: GNU LGPLv3
+// Contributors: yiyungent@gmail.com
+// Project: https://yiyungent.github.io/PluginCore
+// GitHub: https://github.com/yiyungent/PluginCore
+//===================================================
+
+
+
+using Microsoft.Extensions.DependencyInjection;
+
+namespace PluginCore.AspNetCore.BackgroundServices
+{
+ public static class BackgroundServicesHelper
+ {
+ public static IServiceCollection AddBackgroundServices(this IServiceCollection services)
+ {
+ //services.AddScoped(typeof(IHostedService), typeof(TimeBackgroundService));
+ // AddHostedService: Microsoft.AspNetCore.App
+ services.AddHostedService(); // 以这种方式注入就是单例
+
+ return services;
+ }
+ }
+}
+
+
diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/BackgroundServices/PluginTimeJobBackgroundService.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/BackgroundServices/PluginTimeJobBackgroundService.cs
new file mode 100644
index 00000000..0c7c4934
--- /dev/null
+++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/BackgroundServices/PluginTimeJobBackgroundService.cs
@@ -0,0 +1,92 @@
+//===================================================
+// License: GNU LGPLv3
+// Contributors: yiyungent@gmail.com
+// Project: https://yiyungent.github.io/PluginCore
+// GitHub: https://github.com/yiyungent/PluginCore
+//===================================================
+
+
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Linq;
+using System.Threading.Tasks;
+using PluginCore.Interfaces;
+using PluginCore.IPlugins;
+using PluginCore.Utils;
+
+namespace PluginCore.AspNetCore.BackgroundServices
+{
+ public class PluginTimeJobBackgroundService : TimeBackgroundService
+ {
+ ///
+ /// 插件与之最近执行时间
+ /// 最近执行时间: 10位秒 时间戳
+ ///
+ private readonly Dictionary _pluginAndLastExecuteTimeDic = new Dictionary();
+
+ private readonly IPluginFinder _pluginFinder;
+
+ private static readonly object _doWorklock = new object();
+
+ public PluginTimeJobBackgroundService(IPluginFinder pluginFinder)
+ {
+ _pluginFinder = pluginFinder;
+ // 最小间隔 1 秒
+ _timerPeriod = TimeSpan.FromSeconds(1);
+ }
+
+ protected override void DoWork(object state)
+ {
+ lock (_doWorklock)
+ {
+ //Console.WriteLine("Memory used before collection: {0:N0}", GC.GetTotalMemory(false));
+
+ var plugins = this._pluginFinder.EnablePlugins().ToList();
+
+ List enabledPluginKeyList = new List();
+ foreach (var item in plugins)
+ {
+ string pluginKey = item.GetType().ToString();
+ enabledPluginKeyList.Add(pluginKey);
+ if (this._pluginAndLastExecuteTimeDic.ContainsKey(pluginKey))
+ {
+ long lastExecuteTime = this._pluginAndLastExecuteTimeDic[pluginKey];
+ long nowTime = DateTime.Now.ToTimeStamp10();
+ if (nowTime - lastExecuteTime >= item.SecondsPeriod)
+ {
+ // 调用
+ Utils.LogUtil.Info($"{pluginKey}: {nameof(ITimeJobPlugin)}.{nameof(ITimeJobPlugin.ExecuteAsync)}");
+ Task task = item?.ExecuteAsync();
+ this._pluginAndLastExecuteTimeDic[pluginKey] = DateTime.Now.ToTimeStamp10();
+ }
+ }
+ else
+ {
+ // 调用
+ Utils.LogUtil.Info($"{pluginKey}: {nameof(ITimeJobPlugin)}.{nameof(ITimeJobPlugin.ExecuteAsync)}");
+ Task task = item?.ExecuteAsync();
+ this._pluginAndLastExecuteTimeDic.Add(pluginKey, DateTime.Now.ToTimeStamp10());
+ }
+ }
+ // 所有插件遍历结束
+ // 出现在了 _pluginAndLastExecuteTimeDic 中,但没有出现在 enabledPluginKeyList, 说明为之前启用过,但现在已禁用的插件,需要去除掉
+ List keys = this._pluginAndLastExecuteTimeDic.Select(m => m.Key).ToList();
+ foreach (string key in keys)
+ {
+ if (!enabledPluginKeyList.Contains(key))
+ {
+ this._pluginAndLastExecuteTimeDic.Remove(key);
+ }
+ }
+
+ GC.Collect();
+ GC.WaitForPendingFinalizers();
+ //Console.WriteLine("Memory used after full collection: {0:N0}", GC.GetTotalMemory(true));
+ }
+ }
+ }
+}
+
+
diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/BackgroundServices/TimeBackgroundService.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/BackgroundServices/TimeBackgroundService.cs
new file mode 100644
index 00000000..9532e80f
--- /dev/null
+++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/BackgroundServices/TimeBackgroundService.cs
@@ -0,0 +1,47 @@
+//===================================================
+// License: GNU LGPLv3
+// Contributors: yiyungent@gmail.com
+// Project: https://yiyungent.github.io/PluginCore
+// GitHub: https://github.com/yiyungent/PluginCore
+//===================================================
+
+
+
+using Microsoft.Extensions.Hosting;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace PluginCore.AspNetCore.BackgroundServices
+{
+ public abstract class TimeBackgroundService : IHostedService, IDisposable
+ {
+ protected Timer _timer;
+ protected TimeSpan _timerPeriod;
+
+ public virtual Task StartAsync(CancellationToken cancellationToken)
+ {
+ _timer = new Timer(DoWork, null, TimeSpan.Zero, _timerPeriod);
+
+ return Task.CompletedTask;
+ }
+
+ public virtual Task StopAsync(CancellationToken cancellationToken)
+ {
+ _timer?.Change(Timeout.Infinite, 0);
+
+ return Task.CompletedTask;
+ }
+
+ protected abstract void DoWork(object state);
+
+ public virtual void Dispose()
+ {
+ _timer?.Dispose();
+ }
+ }
+}
+
+
diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/CHANGELOG.md b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/CHANGELOG.md
new file mode 100644
index 00000000..4ffa87a0
--- /dev/null
+++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/CHANGELOG.md
@@ -0,0 +1,280 @@
+# Changelog
+
+All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines.
+
+---
+## [unreleased]
+
+### Documentation
+
+- **(CHANGELOG.md)** update - ([85bf0d9](https://github.com/yiyungent/PluginCore/commit/85bf0d9bd8e2f9efa7e8c0db3127ec8aaa054bc8)) - github-actions[bot]
+- **(CHANGELOG.md)** update - ([a329e4e](https://github.com/yiyungent/PluginCore/commit/a329e4e7ec4048ec445e826f4536402abd832e46)) - github-actions[bot]
+- **(CHANGELOG.md)** update - ([0bbfc89](https://github.com/yiyungent/PluginCore/commit/0bbfc8955b7f6338db2125c78ec250e9eeeadcce)) - github-actions[bot]
+- **(CHANGELOG.md)** update - ([4f6b47b](https://github.com/yiyungent/PluginCore/commit/4f6b47b3f86bfce4a8f660166837a7322c568d78)) - github-actions[bot]
+
+### Miscellaneous Chores
+
+- **(src/plugincore.aspnetcore)** cliff.toml, CHANGELOG.md - ([bf95c28](https://github.com/yiyungent/PluginCore/commit/bf95c287ae9902608c4711e4dec05575c2d0e794)) - yiyun
+
+---
+## [PluginCore.AspNetCore-v1.4.3](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.4.2..PluginCore.AspNetCore-v1.4.3) - 2024-08-31
+
+### Bug Fixes
+
+- **(PluginCore.AspNetCore/Controllers/PluginsController)** swagger [FromForm] - ([241d9a7](https://github.com/yiyungent/PluginCore/commit/241d9a72973d9cf1a11c11264a91e9370b2a6cda)) - yiyun
+
+### Features
+
+- **(src/plugincore.aspnetcore/extensions/plugincorestartupextensions.cs)** 插件 wwwroot 默认页 指定, 无需再手动 - ([469ae81](https://github.com/yiyungent/PluginCore/commit/469ae81fc31adce0c0b0701e2dfa1d98634ee184)) - yiyun
+
+### Miscellaneous Chores
+
+- **(PluginCore.AspNetCore.csproj)** 1.4.2 -> 1.4.3 - ([1924e0c](https://github.com/yiyungent/PluginCore/commit/1924e0c179aaac1a1d1f57a8bdebc7578d0b7650)) - yiyun
+
+---
+## [PluginCore.AspNetCore-v1.4.2](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.4.1..PluginCore.AspNetCore-v1.4.2) - 2024-04-06
+
+### Features
+
+- **(src/plugincore.aspnetcore/backgroundservices/plugintimejobbackgroundservice.cs)** log - ([5da04c2](https://github.com/yiyungent/PluginCore/commit/5da04c20b57a166811ef8af828c5fd0bafab844c)) - yiyun
+- **(src/plugincore.aspnetcore/extensions/plugincorestartupextensions.cs)** plugin:startup->appstart - ([71cafe7](https://github.com/yiyungent/PluginCore/commit/71cafe7fa7348b4d8ccb0b342f31ff848c6e77e9)) - yiyun
+
+### Build
+
+- **(src/plugincore.aspnetcore/plugincore.aspnetcore.csproj)** 1.4.1 -> 1.4.2 - ([75f17be](https://github.com/yiyungent/PluginCore/commit/75f17becb01b6d833759811e65cf464ea7a745b1)) - yiyun
+
+---
+## [PluginCore.AspNetCore-v1.4.1](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.4.0..PluginCore.AspNetCore-v1.4.1) - 2024-03-14
+
+### Bug Fixes
+
+- **(src/plugincore.aspnetcore/extensions/plugincorestartupextensions.cs)** log, _serviceProvider - ([7d7a904](https://github.com/yiyungent/PluginCore/commit/7d7a904fd1794e26f278655f6f3f286b92bd1492)) - yiyun
+
+### Build
+
+- **(plugincore.aspnetcore.csproj)** 1.4.0 -> 1.4.1 - ([393d861](https://github.com/yiyungent/PluginCore/commit/393d861870c45a70f63421b5451df76e4ed9c808)) - yiyun
+
+---
+## [PluginCore.AspNetCore-v1.4.0](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.3.4..PluginCore.AspNetCore-v1.4.0) - 2024-02-15
+
+### Features
+
+- **(src/plugincore.aspnetcore)** authentication & Authorize - ([73a673e](https://github.com/yiyungent/PluginCore/commit/73a673e06d2a8d662a44323cf7401e91d814ae90)) - yiyun
+- **(src/plugincore.aspnetcore)** 认证与授权: 优化,分离, PluginCoreStartupExtensions 优化 - ([ebab6d8](https://github.com/yiyungent/PluginCore/commit/ebab6d888a303f221e4a45ddaa28107ff4a012c4)) - yiyun
+- **(src/plugincore.aspnetcore)** accountManager 部分方法静态化, 提供 HttpContext 传入方式, 相关引用处更新调用 - ([491f334](https://github.com/yiyungent/PluginCore/commit/491f334baabb28e1736b5cc1bd19a0081c54949c)) - yiyun
+
+### Miscellaneous Chores
+
+- **(src/plugincore.aspnetcore/extensions/plugincorestartupextensions.cs)** 代码缩进: 美化 - ([6a03628](https://github.com/yiyungent/PluginCore/commit/6a036289ba6f3ae1592b4e4ed58b4fb1bcdc1306)) - yiyun
+
+### Build
+
+- **(plugincore.aspnetcore.csproj)** 1.3.4 -> 1.4.0 - ([f786f5e](https://github.com/yiyungent/PluginCore/commit/f786f5eeb5e19558e3e73890a42f56ce812784cc)) - yiyun
+
+---
+## [PluginCore.AspNetCore-v1.3.4](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.3.3..PluginCore.AspNetCore-v1.3.4) - 2023-12-30
+
+### Bug Fixes
+
+- **(src/plugincore.aspnetcore)** 转向/适配 LogUtil - ([1a7c71d](https://github.com/yiyungent/PluginCore/commit/1a7c71d748fd228f1053de67572a04f466012ab8)) - yiyun
+- **(src/plugincore.aspnetcore)** 适配: LogUtil - ([f150d07](https://github.com/yiyungent/PluginCore/commit/f150d07b55326133afdfe2c156464605483feb05)) - yiyun
+- **(src/plugincore.aspnetcore)** 适配 LogUtil.Error - ([9c82b16](https://github.com/yiyungent/PluginCore/commit/9c82b161c5507ad1a7b78b1191dba208e8f4be45)) - yiyun
+- **(src/plugincore.aspnetcore/middlewares/languagemiddleware.cs)** namespace: 语法降级 - ([a1f83fc](https://github.com/yiyungent/PluginCore/commit/a1f83fce8f1dcf2e5ead3b8a15eae006053af4de)) - yiyun
+
+### Documentation
+
+- **(src/plugincore.aspnetcore/readme.txt)** zh -> EN - ([f822b0b](https://github.com/yiyungent/PluginCore/commit/f822b0b79d1afca536bb7e10814226fb65988365)) - yiyun
+- **(src/plugincore.aspnetcore/readme.txt)** update - ([0a3210b](https://github.com/yiyungent/PluginCore/commit/0a3210bc073252a1f18d0910dc80aabd1061a66f)) - yiyun
+
+### Features
+
+- **(src/**/*.cs)** // License: Apache-2.0 -> // License: GNU LGPLv3 - ([57366d3](https://github.com/yiyungent/PluginCore/commit/57366d3e2afdb8e20e94851aa8a09f1ee61b6d7e)) - yiyun
+- **(src/**/*.cs)** // Project: https://moeci.com/PluginCore -> // Project: https://yiyungent.github.io/PluginCore - ([7420480](https://github.com/yiyungent/PluginCore/commit/742048065978c1b8597fab3d52f011db4247fbda)) - yiyun
+- **(src/plugincore.aspnetcore)** use Constants - ([6cd128a](https://github.com/yiyungent/PluginCore/commit/6cd128a2ce6da83f8cfee46ae03a7af44380e791)) - yiyun
+- **(src/plugincore.aspnetcore)** languageMiddleware: 当前 Language - ([b0d79e7](https://github.com/yiyungent/PluginCore/commit/b0d79e7a0ae469cc295d87ed8a7c97a355cbc7a1)) - yiyun
+- **(src/plugincore.aspnetcore)** 适配: LogUtil - ([b09da39](https://github.com/yiyungent/PluginCore/commit/b09da39199d614f40ec533d7a472069c40121fae)) - yiyun
+- **(src/plugincore.aspnetcore)** 认证与授权: 日志输出: 中文->英文 - ([1470a99](https://github.com/yiyungent/PluginCore/commit/1470a9913fff6e0df37495d1143f5a55eed995fd)) - yiyun
+- **(src/plugincore.aspnetcore/controllers/pluginscontroller.cs)** 启用,禁用: Message: 使用 BasePlugin 源 - ([d6c8a33](https://github.com/yiyungent/PluginCore/commit/d6c8a3361a7573c68031ad6e8bd8aa35c65035d4)) - yiyun
+- **(src/plugincore.aspnetcore/extensions/plugincorestartupextensions.cs)** use Constants - ([68952a1](https://github.com/yiyungent/PluginCore/commit/68952a13613d469cefe9fc9fd13ab7525dd93f78)) - yiyun
+- **(src/plugincore.aspnetcore/extensions/plugincorestartupextensions.cs)** use constants - ([37798ef](https://github.com/yiyungent/PluginCore/commit/37798efc92f82e5e5f4d696b6de5466809a18d48)) - yiyun
+- **(src/plugincore.aspnetcore/extensions/plugincorestartupextensions.cs)** 初始化: Logger - ([6e24a5a](https://github.com/yiyungent/PluginCore/commit/6e24a5a391af60f8ec0ba85e8ea86ea23c49f522)) - yiyun
+- **(src/plugincore.aspnetcore/extensions/plugincorestartupextensions.cs)** log 输出 -> 英文 - ([e41b73a](https://github.com/yiyungent/PluginCore/commit/e41b73adda0ca5d9f1f60109ee8b6fe7de39deda)) - yiyun
+
+### Build
+
+- **(plugincore.aspnetcore.csproj)** 1.3.3 -> 1.3.4 - ([84550cc](https://github.com/yiyungent/PluginCore/commit/84550cc9159c387cedbc8beda282dfcfea9cdcb1)) - yiyun
+
+---
+## [PluginCore.AspNetCore-v1.3.3](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.3.2..PluginCore.AspNetCore-v1.3.3) - 2023-12-14
+
+### Build
+
+- **(src/plugincore.aspnetcore/plugincore.aspnetcore.csproj)** packageReference update, 1.3.2->1.3.3 - ([6fc3d1a](https://github.com/yiyungent/PluginCore/commit/6fc3d1ad6359a5f8690babfc6949e0821d5bd6c5)) - yiyun
+
+---
+## [PluginCore.AspNetCore-v1.3.2](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.3.1..PluginCore.AspNetCore-v1.3.2) - 2023-08-21
+
+### Features
+
+- **(src/plugincore.aspnetcore/extensions/plugincorestartupextensions.cs)** services: PluginFinder - ([144db05](https://github.com/yiyungent/PluginCore/commit/144db05576001a72f9cbd6beea0b3b5baa6a082c)) - yiyun
+
+### Build
+
+- **(src/plugincore.aspnetcore/plugincore.aspnetcore.csproj)** 1.3.2 - ([dfdd17d](https://github.com/yiyungent/PluginCore/commit/dfdd17dbcb2fc636cc102ab76497ff5768c64a4e)) - yiyun
+
+---
+## [PluginCore.AspNetCore-v1.3.1](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.3.0..PluginCore.AspNetCore-v1.3.1) - 2023-02-15
+
+### Build
+
+- **(plugincore.aspnetcore.csproj)** `1.3.1` - ([39e0d03](https://github.com/yiyungent/PluginCore/commit/39e0d037c6f3e0dcf4845b5ca8ae2aa41142a474)) - yiyun
+
+---
+## [PluginCore.AspNetCore-v1.3.0](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.2.0..PluginCore.AspNetCore-v1.3.0) - 2023-02-15
+
+### Bug Fixes
+
+- **(plugincore.aspnetcore,plugincore)** iList EnabledPlugins->List,IList不支持Remove - ([4d5d30e](https://github.com/yiyungent/PluginCore/commit/4d5d30e66c4c28998a7a6ac96bf3ffb25e4872b4)) - yiyun
+
+### Features
+
+- **(plugincore.aspnetcore,plugincore.iplugins,plugincore)** 仅保留已启用/已禁用 状态, IPlugin新方法 - ([e843a5b](https://github.com/yiyungent/PluginCore/commit/e843a5ba9fad4e88290c09bb3282b730c44c5a06)) - yiyun
+
+### Build
+
+- **(src/plugincore.aspnetcore/plugincore.aspnetcore.csproj)** `1.3.0` - ([64b9775](https://github.com/yiyungent/PluginCore/commit/64b977569312539aa2775235ae1f0fca5517ddcd)) - yiyun
+
+---
+## [PluginCore.AspNetCore-v1.2.0](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.1.0..PluginCore.AspNetCore-v1.2.0) - 2023-02-14
+
+### Features
+
+- **(src/plugincore.aspnetcore/controllers/debugcontroller.cs)** pluginContexts:PluginId - ([f82b7b1](https://github.com/yiyungent/PluginCore/commit/f82b7b199420b15fc6f38e8f26c8094ee4be1b88)) - yiyun
+
+### Build
+
+- **(src/plugincore.aspnetcore/plugincore.aspnetcore.csproj)** 1.2.0 - ([fe418e0](https://github.com/yiyungent/PluginCore/commit/fe418e0f67dd8ed1fd2d78dc3f4106c8dc1a396b)) - yiyun
+
+---
+## [PluginCore.AspNetCore-v1.1.0](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.0.4..PluginCore.AspNetCore-v1.1.0) - 2023-02-10
+
+### Features
+
+- **(src/plugincore.aspnetcore/)** update - ([52ee48f](https://github.com/yiyungent/PluginCore/commit/52ee48fe234040fd0cc0a4d21f3c2e1c9483735e)) - yiyun
+- **(src/plugincore.aspnetcore/controllers/debugcontroller.cs)** add - ([196823d](https://github.com/yiyungent/PluginCore/commit/196823d6e3b39eb70d1d4bb55d7d4c8433ee64fc)) - yiyun
+- **(src/plugincore.aspnetcore/controllers/debugcontroller.cs)** 完成 - ([2a2e213](https://github.com/yiyungent/PluginCore/commit/2a2e2131ee4b25912c16ca0d01ef408702cbfc39)) - yiyun
+- **(src/plugincore.aspnetcore/controllers/debugcontroller.cs)** services - ([22edc22](https://github.com/yiyungent/PluginCore/commit/22edc229c1bedc4667a920ef62c11c8e002ba9d2)) - yiyun
+
+### Build
+
+- **(src/plugincore.aspnetcore/plugincore.aspnetcore.csproj)** 1.1.0 - ([3bdb176](https://github.com/yiyungent/PluginCore/commit/3bdb17602c3258386711b200917e567c82dc442b)) - yiyun
+
+---
+## [PluginCore.AspNetCore-v1.0.4](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.0.3..PluginCore.AspNetCore-v1.0.4) - 2023-01-12
+
+### Build
+
+- **(plugincore.aspnetcore.csproj)** 1.0.4 - ([46cf5dd](https://github.com/yiyungent/PluginCore/commit/46cf5dd5f955f6b648323e5216af97dd177e5a4b)) - yiyun
+
+---
+## [PluginCore.AspNetCore-v1.0.3](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.0.2..PluginCore.AspNetCore-v1.0.3) - 2022-06-03
+
+### Bug Fixes
+
+- **(backgroundservices/plugintimejobbackgroundservice.cs)** 定时任务:强制GC回收,抑制内存++ - ([434e824](https://github.com/yiyungent/PluginCore/commit/434e82403b6aa2050945eeaa1131ea7965bdbc5f)) - yiyun
+
+### Build
+
+- **(plugincore.aspnetcore.csproj)** 1.0.3 - ([7da5638](https://github.com/yiyungent/PluginCore/commit/7da5638672571ca0ef057ebf73493260886b903e)) - yiyun
+
+---
+## [PluginCore.AspNetCore-v1.0.2](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.0.1..PluginCore.AspNetCore-v1.0.2) - 2022-04-19
+
+### Features
+
+- **(plugincore.aspnetcore)** pluginsController: 移除: 插件上传大小限制 - ([90f8d67](https://github.com/yiyungent/PluginCore/commit/90f8d671a840ae8ca3688f4a5cbc22e568a6159e)) - yiyun
+
+### Style
+
+- add: copyright: *.cs - ([9643dce](https://github.com/yiyungent/PluginCore/commit/9643dce112861a440d63306cb555accbed3d5111)) - yiyun
+
+### Build
+
+- **(plugincore.aspnetcore.csproj)** 1.0.2 - ([b4eb1cc](https://github.com/yiyungent/PluginCore/commit/b4eb1cca9039ff95a516ba6d4000f10d156e03d0)) - yiyun
+- **(plugincore.aspnetcore/package.json,package-lock.json)** "plugincore-admin-frontend": "0.3.2" - ([7a70b20](https://github.com/yiyungent/PluginCore/commit/7a70b2003128bf569b4fdd4b7eaa662a92da8ee8)) - yiyun
+
+---
+## [PluginCore.AspNetCore-v1.0.1](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v1.0.0..PluginCore.AspNetCore-v1.0.1) - 2022-04-17
+
+### Build
+
+- **(plugincore.aspnetcore.csproj)** 1.0.1 - ([ab348ce](https://github.com/yiyungent/PluginCore/commit/ab348ceca937f1b1f4ba8c83f4816b9a0ae7ea3e)) - yiyun
+
+---
+## [PluginCore.AspNetCore-v1.0.0](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v0.0.5..PluginCore.AspNetCore-v1.0.0) - 2022-04-17
+
+### Bug Fixes
+
+- **(plugincore)** 临时修复由于 PluginContextManager 单例失败 导致的插件信息丢失 - ([fa613b4](https://github.com/yiyungent/PluginCore/commit/fa613b4c46e41c906fe955eb8f62c3f4937795bc)) - yiyun
+
+### Features
+
+- **(plugincore,plugincore.aspnetcore)** aspNetCorePluginManagerBeta,PluginLoadContext,PluginFinder - ([9d65a59](https://github.com/yiyungent/PluginCore/commit/9d65a590e3e0850251f6d815c322c7c5d9c7cf3f)) - yiyun
+- **(plugincore.aspnetcore)** add:DebugController.PluginContext - ([cd8de63](https://github.com/yiyungent/PluginCore/commit/cd8de636c8d7e2b125e11c6ce4091567cb97d4bc)) - yiyun
+- **(plugincore.aspnetcore)** commonResponseModel -> BaseResponseModel - ([1a0e834](https://github.com/yiyungent/PluginCore/commit/1a0e834b7dfdb45c45770d42b1733f1cb449a6ca)) - yiyun
+
+### Refactoring
+
+- **(plugincore.aspnetcore,plugincore)** 未完成 - ([a151bcd](https://github.com/yiyungent/PluginCore/commit/a151bcda125cb7e9b5fe11d44e1389afa7a1db5e)) - yiyun
+- **(plugincore.aspnetcore,plugincore)** 重构v2: 未测试 - ([53dde31](https://github.com/yiyungent/PluginCore/commit/53dde31116bd6455d33f7d7006b6fd1430f3694b)) - yiyun
+- **(plugincore.aspnetcore,plugincore)** 变量名,属性名,类名规范化 - ([eaadabf](https://github.com/yiyungent/PluginCore/commit/eaadabfd759228da245af1d9bd5b86e557540d28)) - yiyun
+
+### Build
+
+- **(plugincore.aspnetcore.csproj)** 1.0.0 - ([9b5caf4](https://github.com/yiyungent/PluginCore/commit/9b5caf4c4cdc543025c5493d35275a836cae61a9)) - yiyun
+
+---
+## [PluginCore.AspNetCore-v0.0.5](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v0.0.4..PluginCore.AspNetCore-v0.0.5) - 2022-04-16
+
+### Build
+
+- **(plugincore.aspnetcore.csproj)** 0.0.5 - ([3e5cb09](https://github.com/yiyungent/PluginCore/commit/3e5cb0921a9faa33f93169c2d983011a1e8c5b5f)) - yiyun
+
+---
+## [PluginCore.AspNetCore-v0.0.4](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v0.0.3..PluginCore.AspNetCore-v0.0.4) - 2022-04-16
+
+### Build
+
+- **(plugincore.aspnetcore.csproj)** 0.0.4 - ([adffab3](https://github.com/yiyungent/PluginCore/commit/adffab3fecee6696709c1d325b8c11e5a5389031)) - yiyun
+
+---
+## [PluginCore.AspNetCore-v0.0.3](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v0.0.2..PluginCore.AspNetCore-v0.0.3) - 2022-04-16
+
+### Bug Fixes
+
+- **(plugincore.aspnetcore/extensions/plugincorestartupextensions.cs)** pluginFinder:TryAddTransient - ([03e9235](https://github.com/yiyungent/PluginCore/commit/03e9235c3c7fe68a3209cb1109792869e78aaa4e)) - yiyun
+- **(plugincore.aspnetcore/extensions/plugincorestartupextensions.cs)** 注释错误: 在程序启动时加载所有 已安装并启用 的插件 - ([844826a](https://github.com/yiyungent/PluginCore/commit/844826a630f99c2d866a98e6556404d4b1857e52)) - yiyun
+
+### Build
+
+- **(plugincore.aspnetcore.csproj)** 0.0.3 - ([ee81a45](https://github.com/yiyungent/PluginCore/commit/ee81a4536af17a85bbebc07bc0c980af8c00ad63)) - yiyun
+
+---
+## [PluginCore.AspNetCore-v0.0.2](https://github.com/yiyungent/PluginCore/compare/PluginCore.AspNetCore-v0.0.1..PluginCore.AspNetCore-v0.0.2) - 2022-04-16
+
+### Refactoring
+
+- 1.提取出 PluginCore.AspNetCore,PluginCore.IPlugins.AspNetCore 2.提取出更多接口,可自由替换 - ([fffd8d9](https://github.com/yiyungent/PluginCore/commit/fffd8d91c23fd6e4a4d09cbf91975beb3cf7acf0)) - yiyun
+
+### Build
+
+- **(plugincore.aspnetcore.csproj)** 0.0.2 - ([ba7b274](https://github.com/yiyungent/PluginCore/commit/ba7b27496a4ba784c271305489b9f2f8e6f74ad3)) - yiyun
+
+---
+## [PluginCore.AspNetCore-v0.0.1](https://github.com/yiyungent/PluginCore/compare/PluginCore-v0.9.3..PluginCore.AspNetCore-v0.0.1) - 2022-03-26
+
+### Features
+
+- **(plugincore.aspnetcore)** pluginCore.AspNetCore,PluginCore.AspNetCore-nuget-push.yml - ([491f63e](https://github.com/yiyungent/PluginCore/commit/491f63e3362129a2239d87b090aa04cc2e414e9a)) - yiyun
+
+
diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/AppCenterController.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/AppCenterController.cs
new file mode 100644
index 00000000..762495f5
--- /dev/null
+++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/AppCenterController.cs
@@ -0,0 +1,207 @@
+//===================================================
+// License: GNU LGPLv3
+// Contributors: yiyungent@gmail.com
+// Project: https://yiyungent.github.io/PluginCore
+// GitHub: https://github.com/yiyungent/PluginCore
+//===================================================
+
+
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+using PluginCore;
+using PluginCore.Models;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using PluginCore.AspNetCore.Authorization;
+using PluginCore.AspNetCore.ResponseModel;
+//using ResponseModel;
+
+
+namespace PluginCore.AspNetCore.Controllers
+{
+ ///
+ /// 应用中心
+ /// 插件
+ ///
+ [Route("api/plugincore/admin/[controller]/[action]")]
+ [PluginCoreAdminAuthorize]
+ [ApiController]
+ public class AppCenterController : ControllerBase
+ {
+ #region Fields
+
+ private static Dictionary _pluginDownloadTasks;
+
+ #endregion
+
+ #region Ctor
+
+ static AppCenterController()
+ {
+ _pluginDownloadTasks = new Dictionary();
+ }
+
+ public AppCenterController()
+ {
+
+ }
+ #endregion
+
+ #region Actions
+
+ #region 插件列表
+ ///
+ /// 插件
+ ///
+ ///
+ ///
+ [HttpGet, HttpPost]
+ public async Task> Plugins(string query = "")
+ {
+ BaseResponseModel responseDTO = new BaseResponseModel();
+ IList pluginRegistryModels = new List();
+ try
+ {
+ // 1. TODO: 从json文件中读取插件订阅源 registry url
+ string registryUrl = "";
+ // 2. TODO: 向订阅源发送 http get 获取插件列表信息 eg: http://rem-core-plugins-registry.moeci.com/?query=xxx
+ IList remotePluginIds = new List();
+
+ // 3. 根据本地已有 PluginId 插件情况 状态赋值
+ PluginConfigModel pluginConfigModel = PluginConfigModelFactory.Create();
+ // IList localPluginIds = pluginConfigModel.EnabledPlugins.Concat(pluginConfigModel.DisabledPlugins).Concat(pluginConfigModel.UninstalledPlugins).ToList();
+ IList localPluginIds = PluginPathProvider.AllPluginFolderName();
+
+
+
+ responseDTO.Code = 1;
+ responseDTO.Message = "获取远程插件数据成功";
+ responseDTO.Data = pluginRegistryModels;
+ }
+ catch (Exception ex)
+ {
+ responseDTO.Code = -1;
+ responseDTO.Message = "获取远程插件数据失败: " + ex.Message;
+ responseDTO.Data = pluginRegistryModels;
+ }
+
+ return await Task.FromResult(responseDTO);
+ }
+ #endregion
+
+ #region 下载插件
+ [HttpGet, HttpPost]
+ public async Task> DownloadPlugin(string pluginDownloadUrl = "")
+ {
+ BaseResponseModel responseDTO = new BaseResponseModel();
+
+ #region 效验
+ if (string.IsNullOrEmpty(pluginDownloadUrl))
+ {
+ responseDTO.Code = -1;
+ responseDTO.Message = "插件下载地址不正确";
+ return responseDTO;
+ }
+ // TODO: 效验是否本地已经存在相同pluginId的插件
+
+ #endregion
+
+ try
+ {
+ // 1.执行下载操作, TODO:存在问题,阻塞对性能不好,但不阻塞又不好通知用户插件下载进度,以及可能存在在插件下载过程中,用户再次点击下载
+ WebClient webClient = new WebClient();
+ // TODO: 插件下载文件路径
+ string pluginDownloadFilePath = "";
+ //webClient.DownloadFileAsync(new Uri(pluginDownloadFilePath), "");
+ Task task = webClient.DownloadFileTaskAsync(pluginDownloadUrl, pluginDownloadFilePath);
+
+ _pluginDownloadTasks.Add(pluginDownloadUrl, task);
+
+ webClient.DownloadFileCompleted += Plugin_DownloadFileCompleted;
+ webClient.DownloadProgressChanged += Plugin_DownloadProgressChanged;
+ webClient.Disposed += WebClient_Disposed;
+
+ responseDTO.Code = 1;
+ responseDTO.Message = "开始下载插件";
+ }
+ catch (Exception ex)
+ {
+ responseDTO.Code = -1;
+ responseDTO.Message = "下载插件失败: " + ex.Message;
+ }
+
+ return await Task.FromResult(responseDTO);
+ }
+
+
+
+
+ #endregion
+
+ #region 获取插件下载进度
+ [HttpGet, HttpPost]
+ public async Task> DownloadPluginProgress()
+ {
+ BaseResponseModel responseDTO = new BaseResponseModel();
+ try
+ {
+ responseDTO.Data = new { };
+
+
+
+ responseDTO.Code = 1;
+ responseDTO.Message = "获取插件下载进度成功";
+ }
+ catch (Exception ex)
+ {
+ responseDTO.Code = -1;
+ responseDTO.Message = "获取插件下载进度失败: " + ex.Message;
+ }
+
+ return await Task.FromResult(responseDTO);
+ }
+ #endregion
+
+ #endregion
+
+ #region Helpers
+
+ ///
+ /// 插件下载完成
+ ///
+ ///
+ ///
+ private void Plugin_DownloadFileCompleted(object sender, System.ComponentModel.AsyncCompletedEventArgs e)
+ {
+ Utils.LogUtil.Info("插件下载完成");
+ // 1.从 _pluginDownloadTasks 中移除
+ //_pluginDownloadTasks.Remove();
+ // 2. 解压插件
+
+ }
+
+ private void Plugin_DownloadProgressChanged(object sender, DownloadProgressChangedEventArgs e)
+ {
+ Utils.LogUtil.Info($"插件下载进度改变: {e.ProgressPercentage}% {e.BytesReceived}/{e.TotalBytesToReceive}");
+ }
+
+ private void WebClient_Disposed(object sender, EventArgs e)
+ {
+ if (sender is WebClient webClient)
+ {
+ Utils.LogUtil.Info(webClient.BaseAddress);
+ }
+
+ Utils.LogUtil.Info(nameof(WebClient_Disposed) + ": " + sender.ToString());
+ }
+
+ #endregion
+
+ }
+}
+
+
diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/DebugController.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/DebugController.cs
new file mode 100644
index 00000000..a3ef7fc5
--- /dev/null
+++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/DebugController.cs
@@ -0,0 +1,280 @@
+using System.Runtime.CompilerServices;
+//===================================================
+// License: GNU LGPLv3
+// Contributors: yiyungent@gmail.com
+// Project: https://yiyungent.github.io/PluginCore
+// GitHub: https://github.com/yiyungent/PluginCore
+//===================================================
+
+
+
+using Microsoft.AspNetCore.Mvc;
+using PluginCore.AspNetCore.Authorization;
+using PluginCore.AspNetCore.ResponseModel;
+using PluginCore.Interfaces;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using System.Linq;
+using System.Runtime.Loader;
+using Microsoft.Extensions.DependencyInjection;
+using System.Collections.Concurrent;
+using PluginCore.AspNetCore.Extensions;
+
+namespace PluginCore.AspNetCore.Controllers
+{
+ ///
+ /// [ASP.NET Core — 依赖注入\_啊晚的博客-CSDN博客\_asp.net core 依赖注入](https://blog.csdn.net/weixin_37648525/article/details/127942292)
+ /// [ASP.NET Core中的依赖注入(3): 服务的注册与提供 - Artech - 博客园](https://www.cnblogs.com/artech/p/asp-net-core-di-register.html)
+ /// [ASP.NET Core中的依赖注入(5): ServiceProvider实现揭秘 【总体设计 】 - Artech - 博客园](https://www.cnblogs.com/artech/p/asp-net-core-di-service-provider-1.html)
+ /// [dotnet/ServiceProvider.cs at main · dotnet/dotnet](https://github.com/dotnet/dotnet/blob/main/src/runtime/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceProvider.cs)
+ /// [Net6 DI源码分析Part2 Engine,ServiceProvider - 一身大膘 - 博客园](https://www.cnblogs.com/hts92/p/15800990.html)
+ /// [【特别的骚气】asp.net core运行时注入服务,实现类库热插拔 - 四处观察 - 博客园](https://www.cnblogs.com/1996-Chinese-Chen/p/16154218.html)
+ ///
+ /// ActivatorUtilities.CreateInstance(serviceProvider, "test");
+ /// ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider);
+ ///
+ ///
+ [Route("api/plugincore/admin/[controller]/[action]")]
+ [PluginCoreAdminAuthorize]
+ [ApiController]
+ public class DebugController : ControllerBase
+ {
+ #region Fields
+ private readonly IPluginContextManager _pluginContextManager;
+ #endregion
+
+ #region Ctor
+ public DebugController(IPluginContextManager pluginContextManager)
+ {
+ _pluginContextManager = pluginContextManager;
+ }
+ #endregion
+
+ #region Actions
+
+ [HttpGet, HttpPost]
+ public async Task> PluginContexts()
+ {
+ BaseResponseModel responseModel = new BaseResponseModel();
+ try
+ {
+ var pluginContextList = _pluginContextManager.All();
+ Dictionary> keyValuePairs = new Dictionary>();
+ foreach (var pluginContext in pluginContextList)
+ {
+ keyValuePairs.Add($"{pluginContext.GetType().ToString()} - {pluginContext.PluginId} - {pluginContext.GetHashCode()}", pluginContext.Assemblies.Select(m => m.FullName).ToList());
+ }
+
+ responseModel.Code = 1;
+ responseModel.Message = "success";
+ responseModel.Data = keyValuePairs;
+ }
+ catch (Exception ex)
+ {
+ responseModel.Code = -1;
+ responseModel.Message = "error";
+ responseModel.Data = ex.ToString();
+ }
+
+ return await Task.FromResult(responseModel);
+ }
+
+ [HttpGet, HttpPost]
+ public async Task> AssemblyLoadContexts()
+ {
+ BaseResponseModel responseModel = new BaseResponseModel();
+ try
+ {
+ var assemblyLoadContextDefault = AssemblyLoadContext.Default;
+ var assemblyLoadContextAll = AssemblyLoadContext.All;
+ var responseDataModel = new AssemblyLoadContextsResponseDataModel();
+ responseDataModel.Default = new AssemblyLoadContextsResponseDataModel.AssemblyLoadContextModel
+ {
+ Name = assemblyLoadContextDefault.Name,
+ Type = assemblyLoadContextDefault.GetType().ToString(),
+ Assemblies = assemblyLoadContextDefault.Assemblies.Select(m => new AssemblyModel { FullName = m.FullName, DefinedTypes = m.DefinedTypes.Select(m => m.FullName).ToList() }).ToList()
+ };
+ responseDataModel.All = assemblyLoadContextAll.Select(item => new AssemblyLoadContextsResponseDataModel.AssemblyLoadContextModel
+ {
+ Name = item.Name,
+ Type = item.GetType().ToString(),
+ Assemblies = item.Assemblies.Select(m => new AssemblyModel { FullName = m.FullName, DefinedTypes = m.DefinedTypes.Select(m => m.FullName).ToList() }).ToList()
+ }).ToList();
+
+ responseModel.Code = 1;
+ responseModel.Message = "success";
+ responseModel.Data = responseDataModel;
+ }
+ catch (Exception ex)
+ {
+ responseModel.Code = -1;
+ responseModel.Message = "error";
+ responseModel.Data = ex.ToString();
+ }
+
+ return await Task.FromResult(responseModel);
+ }
+
+ [HttpGet, HttpPost]
+ public async Task> Assemblies()
+ {
+ BaseResponseModel responseModel = new BaseResponseModel();
+ try
+ {
+ var assemblies = AppDomain.CurrentDomain.GetAssemblies();
+ List assemblyModels = new List();
+ foreach (var item in assemblies)
+ {
+ assemblyModels.Add(new AssemblyModel
+ {
+ FullName = item.FullName,
+ DefinedTypes = item.DefinedTypes.Select(m => m.FullName).ToList()
+ });
+ }
+
+ responseModel.Code = 1;
+ responseModel.Message = "success";
+ responseModel.Data = assemblyModels;
+ }
+ catch (Exception ex)
+ {
+ responseModel.Code = -1;
+ responseModel.Message = "error";
+ responseModel.Data = ex.ToString();
+ }
+
+ return await Task.FromResult(responseModel);
+ }
+
+ [HttpGet, HttpPost]
+ public async Task> Services([FromServices] IServiceProvider serviceProvider)
+ {
+ BaseResponseModel responseModel = new BaseResponseModel();
+ try
+ {
+ //IServiceProvider serviceProvider = HttpContext.RequestServices;
+ //var provider = serviceProvider.GetType().GetProperty("RootProvider", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+ //var serviceField = provider.GetType().GetField("_realizedServices", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
+ //var serviceValue = serviceField.GetValue(provider);
+ //var funcType = serviceField.FieldType.GetGenericArguments()[1].GetGenericArguments()[0];
+ //ConcurrentDictionary> realizedServices = (ConcurrentDictionary>)serviceValue;
+
+ // 获取所有已经注册的服务
+ var allService = serviceProvider.GetAllServiceDescriptors();
+
+ List serviceModels = new List();
+ foreach (var item in allService)
+ {
+ serviceModels.Add(new ServiceModel
+ {
+ Type = item.Key.ToString(),
+ ImplementationType = item.Value.ImplementationType?.ToString() ?? "",
+ Lifetime = item.Value.Lifetime.ToString(),
+ TypeAssembly = new AssemblyModel
+ {
+ FullName = item.Key.Assembly.FullName,
+ },
+ ImplementationTypeAssembly = new AssemblyModel
+ {
+ FullName = item.Value.ImplementationType?.Assembly?.FullName ?? ""
+ }
+ });
+ }
+
+ responseModel.Code = 1;
+ responseModel.Message = "success";
+ responseModel.Data = serviceModels;
+ }
+ catch (Exception ex)
+ {
+ responseModel.Code = -1;
+ responseModel.Message = "error";
+ responseModel.Data = ex.ToString();
+ }
+
+ return await Task.FromResult(responseModel);
+ }
+
+ #endregion
+
+ public sealed class AssemblyLoadContextsResponseDataModel
+ {
+ public AssemblyLoadContextModel Default
+ {
+ get; set;
+ }
+
+ public List All
+ {
+ get; set;
+ }
+
+ public sealed class AssemblyLoadContextModel
+ {
+ public string Name
+ {
+ get; set;
+ }
+ public string Type
+ {
+ get; set;
+ }
+ public List Assemblies
+ {
+ get; set;
+ }
+ }
+ }
+
+ public sealed class AssembliesResponseDataModel
+ {
+
+ }
+
+ public sealed class ServiceModel
+ {
+ public string Type
+ {
+ get; set;
+ }
+
+ public string ImplementationType
+ {
+ get; set;
+ }
+
+ public string Lifetime
+ {
+ get; set;
+ }
+
+ public AssemblyModel TypeAssembly
+ {
+ get; set;
+ }
+
+ public AssemblyModel ImplementationTypeAssembly
+ {
+ get; set;
+ }
+ }
+
+ public sealed class AssemblyModel
+ {
+ public string FullName
+ {
+ get; set;
+ }
+
+ public List DefinedTypes
+ {
+ get; set;
+ }
+ }
+
+ }
+}
+
+
diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/HomeController.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/HomeController.cs
new file mode 100644
index 00000000..8b093c30
--- /dev/null
+++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/HomeController.cs
@@ -0,0 +1,73 @@
+//===================================================
+// License: GNU LGPLv3
+// Contributors: yiyungent@gmail.com
+// Project: https://yiyungent.github.io/PluginCore
+// GitHub: https://github.com/yiyungent/PluginCore
+//===================================================
+
+
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Net.Http;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Mvc;
+
+namespace PluginCore.AspNetCore.Controllers
+{
+ [Controller]
+ public class HomeController : Controller
+ {
+ #region Old
+ //private readonly IWebHostEnvironment _webHostEnvironment;
+
+ //public bool IsLocalFronted
+ //{
+ // get
+ // {
+ // return PluginCore.Config.PluginCoreConfigFactory.Create().IsLocalFrontend;
+ // }
+ //}
+
+ //public string RemoteFronted
+ //{
+ // get
+ // {
+ // return PluginCore.Config.PluginCoreConfigFactory.Create().RemoteFrontend;
+ // }
+ //}
+
+ //public HomeController(IWebHostEnvironment webHostEnvironment)
+ //{
+ // this._webHostEnvironment = webHostEnvironment;
+ //}
+
+ //[Route("PluginCore/Admin")]
+ //public async Task Home()
+ //{
+ // if (this.IsLocalFronted)
+ // {
+ // var localIndexFilePath = Path.Combine(
+ // this._webHostEnvironment.ContentRootPath, "PluginCoreAdmin", "index.html");
+
+ // return PhysicalFile(localIndexFilePath, "text/html");
+ // }
+ // else
+ // {
+ // string htmlStr = string.Empty;
+ // HttpClient httpClient = new HttpClient();
+ // htmlStr = await httpClient.GetStringAsync(this.RemoteFronted + "/index.html");
+
+ // return Content(htmlStr, "text/html", Encoding.UTF8);
+ // }
+ //}
+ #endregion
+
+ }
+}
+
+
diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/PluginWidgetController.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/PluginWidgetController.cs
new file mode 100644
index 00000000..0b00b6d4
--- /dev/null
+++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/PluginWidgetController.cs
@@ -0,0 +1,103 @@
+//===================================================
+// License: GNU LGPLv3
+// Contributors: yiyungent@gmail.com
+// Project: https://yiyungent.github.io/PluginCore
+// GitHub: https://github.com/yiyungent/PluginCore
+//===================================================
+
+
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Threading.Tasks;
+using PluginCore;
+using PluginCore.Models;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using PluginCore.AspNetCore.Authorization;
+using PluginCore.AspNetCore.ResponseModel;
+using PluginCore.IPlugins;
+using System.Text;
+using PluginCore.Interfaces;
+
+namespace PluginCore.AspNetCore.Controllers
+{
+ [Route("api/plugincore/[controller]/[action]")]
+ [ApiController]
+ public class PluginWidgetController : ControllerBase
+ {
+ #region Fields
+ private readonly IPluginFinder _pluginFinder;
+ #endregion
+
+ #region Ctor
+ public PluginWidgetController(IPluginFinder pluginFinder)
+ {
+ _pluginFinder = pluginFinder;
+ }
+ #endregion
+
+ #region Actions
+
+ #region Widget
+ ///
+ /// Widget
+ ///
+ ///
+ [HttpGet, HttpPost]
+ //public async Task> Widget(string widgetKey, string extraPars = "")
+ public async Task Widget(string widgetKey, string extraPars = "")
+ {
+ BaseResponseModel responseModel = new ResponseModel.BaseResponseModel();
+ string responseData = "";
+ widgetKey = widgetKey.Trim('"', '\'');
+ string[] extraParsArr = null;
+ if (!string.IsNullOrEmpty(extraPars))
+ {
+ extraParsArr = extraPars.Split(",", StringSplitOptions.RemoveEmptyEntries);
+ extraParsArr = extraParsArr.Select(m => m.Trim('"', '\'')).ToArray();
+ }
+ StringBuilder sb = new StringBuilder();
+ sb.AppendLine($"");
+ try
+ {
+ List plugins = this._pluginFinder.EnablePlugins().ToList();
+ foreach (var item in plugins)
+ {
+ string widgetStr = await item.Widget(widgetKey, extraParsArr);
+ if (!string.IsNullOrEmpty(widgetStr))
+ {
+ // TODO: 配合 PluginCoreConfig.PluginWidgetDebug
+ // TODO: PluginCoreConfig 改为 Options 模式, 避免手动反复读取文件 效率低
+ //sb.AppendLine($"");
+
+ sb.AppendLine(widgetStr);
+ }
+ }
+
+ }
+ catch (Exception ex)
+ {
+ Utils.LogUtil.Error(ex, ex.Message);
+ sb.AppendLine($"");
+ }
+ sb.AppendLine($"");
+ responseData = sb.ToString();
+
+ responseModel.Code = 1;
+ responseModel.Message = "Load Widget Success";
+ responseModel.Data = responseData;
+
+ //return await Task.FromResult(responseModel);
+ return Content(responseData, "text/html;charset=utf-8");
+ }
+ #endregion
+
+ #endregion
+
+ }
+}
+
+
diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/PluginsController.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/PluginsController.cs
new file mode 100644
index 00000000..d6ddec27
--- /dev/null
+++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/PluginsController.cs
@@ -0,0 +1,688 @@
+//===================================================
+// License: GNU LGPLv3
+// Contributors: yiyungent@gmail.com
+// Project: https://yiyungent.github.io/PluginCore
+// GitHub: https://github.com/yiyungent/PluginCore
+//===================================================
+
+
+
+using System;
+using System.Collections.Generic;
+using System.Configuration;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+//using Core.Common;
+//using Framework.Authorization;
+using PluginCore;
+using PluginCore.Models;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using PluginCore.AspNetCore.Authorization;
+using PluginCore.Infrastructure;
+using PluginCore.IPlugins;
+using PluginCore.AspNetCore.ResponseModel;
+using PluginCore.Interfaces;
+using PluginCore.AspNetCore.Interfaces;
+
+//using ResponseModel;
+
+namespace PluginCore.AspNetCore.Controllers
+{
+ [Route("api/plugincore/admin/[controller]/[action]")]
+ [PluginCoreAdminAuthorize]
+ [ApiController]
+ public class PluginsController : ControllerBase
+ {
+ #region Fields
+ private readonly IPluginManager _pluginManager;
+ private readonly IPluginFinder _pluginFinder;
+ private readonly IPluginApplicationBuilderManager _pluginApplicationBuilderManager;
+ #endregion
+
+ #region Ctor
+ public PluginsController(IPluginManager pluginManager, IPluginFinder pluginFinder, IPluginApplicationBuilderManager pluginApplicationBuilderManager)
+ {
+ _pluginManager = pluginManager;
+ _pluginFinder = pluginFinder;
+ _pluginApplicationBuilderManager = pluginApplicationBuilderManager;
+ }
+ #endregion
+
+ #region Actions
+
+ #region 插件列表
+ ///
+ /// 加载插件列表
+ ///
+ /// 插件状态
+ ///
+ [HttpGet, HttpPost]
+ public async Task> List(string status = "all")
+ {
+ BaseResponseModel responseData = new ResponseModel.BaseResponseModel();
+ var pluginConfigModel = PluginConfigModelFactory.Create();
+
+ // 获取所有插件信息
+ IList pluginInfoModels = PluginInfoModelFactory.CreateAll();
+ IList responseModels = new List();
+ string[] enablePluginIds = _pluginFinder.EnablePluginIds().ToArray();
+
+ // 添加插件状态
+ responseModels = PluginInfoModelToResponseModel(pluginInfoModels, pluginConfigModel, enablePluginIds);
+ #region 筛选插件状态
+ switch (status.ToLower())
+ {
+ case "all":
+ break;
+ case "enabled":
+ responseModels = responseModels.Where(m => m.Status == PluginStatus.Enabled).ToList();
+ break;
+ case "disabled":
+ responseModels = responseModels.Where(m => m.Status == PluginStatus.Disabled).ToList();
+ break;
+ default:
+ break;
+ }
+ #endregion
+
+ responseData.Code = 1;
+ responseData.Message = "加载插件列表成功";
+ responseData.Data = responseModels;
+
+ return await Task.FromResult(responseData);
+ }
+ #endregion
+
+ #region 卸载插件
+ [HttpGet, HttpPost]
+ public async Task> Uninstall(string pluginId)
+ {
+ BaseResponseModel responseData = new BaseResponseModel();
+ pluginId = pluginId.Trim();
+ var pluginConfigModel = PluginConfigModelFactory.Create();
+ // 卸载插件 必须 先禁用插件
+ #region 效验
+ if (pluginConfigModel.EnabledPlugins.Contains(pluginId))
+ {
+ responseData.Code = -1;
+ responseData.Message = "卸载失败: 请先禁用此插件";
+ return await Task.FromResult(responseData);
+ }
+ string pluginDirStr= Path.Combine(PluginPathProvider.PluginsRootPath(), pluginId);
+ string pluginWwwrootDirStr = Path.Combine(PluginPathProvider.PluginsWwwRootDir(), pluginId);
+ if (!Directory.Exists(pluginDirStr) && !Directory.Exists(pluginWwwrootDirStr))
+ {
+ responseData.Code = -2;
+ responseData.Message = "卸载失败: 此插件不存在";
+ return await Task.FromResult(responseData);
+ }
+ #endregion
+
+ try
+ {
+ // PS:卸载插件必须先禁用插件,所以此时插件LoadContext已被移除释放(插件Assemblies已被释放), 此处不需移除LoadContext
+
+ // 1.删除物理文件
+ var pluginDir = new DirectoryInfo(pluginDirStr);
+ if (pluginDir.Exists) {
+ pluginDir.Delete(true);
+ }
+ // 虽然 已禁用 时 pluginWwwrootDirStr/pluginId 已删除, 但为确保, 还是再删除一次
+ var pluginWwwrootDir = new DirectoryInfo(pluginWwwrootDirStr);
+ if (pluginWwwrootDir.Exists) {
+ pluginWwwrootDir.Delete(true);
+ }
+
+ responseData.Code = 1;
+ responseData.Message = "卸载成功";
+ }
+ catch (Exception ex)
+ {
+ responseData.Code = -1;
+ responseData.Message = "卸载失败: " + ex.Message;
+ }
+
+ return await Task.FromResult(responseData);
+ }
+ #endregion
+
+ #region 启用插件
+ [HttpGet, HttpPost]
+ public async Task> Enable(string pluginId)
+ {
+ BaseResponseModel responseData = new BaseResponseModel();
+ var pluginConfigModel = PluginConfigModelFactory.Create();
+ // 效验是否存在于 已禁用插件列表
+ #region 效验
+ pluginId = pluginId.Trim();
+ var pluginDir = new DirectoryInfo(Path.Combine(PluginPathProvider.PluginsRootPath(), pluginId));
+ if (pluginDir != null && !pluginDir.Exists)
+ {
+ responseData.Code = -1;
+ responseData.Message = "启用失败: 此插件不存在";
+ return await Task.FromResult(responseData);
+ }
+ string[] enablePluginIds = _pluginFinder.EnablePluginIds().ToArray();
+ if (enablePluginIds.Contains(pluginId)) {
+ responseData.Code = -2;
+ responseData.Message = "启用失败: 此插件已启用";
+ return await Task.FromResult(responseData);
+ }
+ #endregion
+
+ try
+ {
+ // 1. 创建插件程序集加载上下文, 添加到 PluginsLoadContexts
+ _pluginManager.LoadPlugin(pluginId);
+ // 2. 添加到 pluginConfigModel.EnabledPlugins
+ pluginConfigModel.EnabledPlugins.Add(pluginId);
+ // 4.保存到 plugin.config.json
+ PluginConfigModelFactory.Save(pluginConfigModel);
+
+ // 5. 找到此插件实例
+ IPlugin plugin = _pluginFinder.Plugin(pluginId);
+ if (plugin == null)
+ {
+ // 7.启用不成功, 回滚插件状态: (1)释放插件上下文 (2)更新 plugin.config.json
+ try
+ {
+ _pluginManager.UnloadPlugin(pluginId);
+ }
+ catch (Exception ex)
+ { }
+
+ // 从 pluginConfigModel.EnabledPlugins 移除
+ pluginConfigModel.EnabledPlugins.Remove(pluginId);
+ // 保存到 plugin.config.json
+ PluginConfigModelFactory.Save(pluginConfigModel);
+
+ responseData.Code = -1;
+ responseData.Message = "启用失败: 此插件不存在";
+ return await Task.FromResult(responseData);
+ }
+ // 6.调取插件的 AfterEnable(), 插件开发者可在此回收资源
+ var pluginEnableResult = plugin.AfterEnable();
+ if (!pluginEnableResult.IsSuccess)
+ {
+ // 7.启用不成功, 回滚插件状态: (1)释放插件上下文 (2)更新 plugin.config.json
+ try
+ {
+ _pluginManager.UnloadPlugin(pluginId);
+ }
+ catch (Exception ex)
+ { }
+
+ // 从 pluginConfigModel.EnabledPlugins 移除
+ pluginConfigModel.EnabledPlugins.Remove(pluginId);
+ // 保存到 plugin.config.json
+ PluginConfigModelFactory.Save(pluginConfigModel);
+
+ responseData.Code = -1;
+ responseData.Message = "启用失败: 来自插件的错误信息: " + pluginEnableResult.Message;
+ return await Task.FromResult(responseData);
+ }
+
+ // 7. ReBuild
+ this._pluginApplicationBuilderManager.ReBuild();
+
+ // 8. 尝试复制 插件下的 wwwroot 到 Plugins_wwwroot
+ string wwwRootDir = PluginPathProvider.WwwRootDir(pluginId);
+ if (Directory.Exists(wwwRootDir))
+ {
+ string targetDir = PluginPathProvider.PluginWwwRootDir(pluginId);
+ Utils.FileUtil.CopyFolder(wwwRootDir, targetDir);
+ }
+
+ responseData.Code = 1;
+ // responseData.Message = "启用成功";
+ responseData.Message = pluginEnableResult.Message;
+ }
+ catch (Exception ex)
+ {
+ responseData.Code = -2;
+ responseData.Message = "启用失败: " + ex.Message;
+ Utils.LogUtil.Error(ex, ex.Message);
+ }
+
+ return await Task.FromResult(responseData);
+ }
+ #endregion
+
+ #region 禁用插件
+ [HttpGet, HttpPost]
+ public async Task> Disable(string pluginId)
+ {
+ BaseResponseModel responseData = new BaseResponseModel();
+ #region 效验
+ pluginId = pluginId.Trim();
+ var pluginConfigModel = PluginConfigModelFactory.Create();
+ // string[] enablePluginIds = _pluginFinder.EnablePluginIds().ToArray();
+ // // 效验是否存在于 已启用插件列表
+ // if (!enablePluginIds.Contains(pluginId))
+ // {
+ // responseData.Code = -1;
+ // responseData.Message = "禁用失败: 此插件不存在, 或未启用";
+ // return await Task.FromResult(responseData);
+ // }
+ #endregion
+
+ try
+ {
+ // 1. 找到此插件实例
+ IPlugin plugin = _pluginFinder.Plugin(pluginId);
+ if (plugin == null)
+ {
+ responseData.Code = -1;
+ responseData.Message = "禁用失败: 此插件不存在, 或未启用";
+ return await Task.FromResult(responseData);
+ }
+ string pluginDisableResultMessage = "";
+ try
+ {
+ // 2.调取插件的 BeforeDisable(), 插件开发者可在此回收资源
+ var pluginDisableResult = plugin.BeforeDisable();
+ pluginDisableResultMessage = pluginDisableResult.Message;
+ if (!pluginDisableResult.IsSuccess)
+ {
+ responseData.Code = -1;
+ responseData.Message = "禁用失败: 来自插件的错误信息: " + pluginDisableResult.Message;
+ return await Task.FromResult(responseData);
+ }
+ // 3.移除插件对应的程序集加载上下文
+ _pluginManager.UnloadPlugin(pluginId);
+ // 3.1. ReBuild
+ this._pluginApplicationBuilderManager.ReBuild();
+ if (pluginConfigModel.EnabledPlugins.Contains(pluginId)) {
+ // 4.从 pluginConfigModel.EnabledPlugins 移除
+ pluginConfigModel.EnabledPlugins.Remove(pluginId);
+ // 5.保存到 plugin.config.json
+ PluginConfigModelFactory.Save(pluginConfigModel);
+ }
+ }
+ catch (Exception ex)
+ {
+ Utils.LogUtil.Error(ex, ex.Message);
+ responseData.Code = -1;
+ responseData.Message = "禁用失败: 此插件不存在, 或未启用";
+ return await Task.FromResult(responseData);
+ }
+
+ // 7. 尝试移除 Plugins_wwwroot/PluginId
+ string pluginWwwRootDir = PluginPathProvider.PluginWwwRootDir(pluginId);
+ if (Directory.Exists(pluginWwwRootDir))
+ {
+ Directory.Delete(pluginWwwRootDir, true);
+ }
+
+
+ responseData.Code = 1;
+ // responseData.Message = "禁用成功";
+ responseData.Message = pluginDisableResultMessage;
+ }
+ catch (Exception ex)
+ {
+ responseData.Code = -2;
+ responseData.Message = "禁用失败: " + ex.Message;
+ Utils.LogUtil.Error(ex, ex.Message);
+ }
+
+ return await Task.FromResult(responseData);
+ }
+ #endregion
+
+ #region 上传插件
+ ///
+ /// 上传插件
+ ///
+ /// 注意: 参数名一定为 file, 对应前端传过来时以 file 为名
+ ///
+ [HttpGet, HttpPost]
+ public async Task> Upload(IFormFile file)
+ {
+ BaseResponseModel responseData = new BaseResponseModel();
+
+ #region 效验
+ if (file == null)
+ {
+ responseData.Code = -1;
+ responseData.Message = "上传的文件不能为空";
+ return responseData;
+ }
+ //文件后缀
+ string fileExtension = Path.GetExtension(file.FileName);//获取文件格式,拓展名
+ // 类型标记
+ UploadFileType uploadFileType = UploadFileType.NoAllowedType;
+ switch (fileExtension)
+ {
+ case ".zip":
+ uploadFileType = UploadFileType.Zip;
+ break;
+ case ".nupkg":
+ uploadFileType = UploadFileType.Nupkg;
+ break;
+ }
+
+ if (fileExtension != ".zip" && fileExtension != ".nupkg")
+ {
+ responseData.Code = -1;
+ // nupkg 其实就是 zip
+ responseData.Message = "只能上传 zip 或 nupkg 格式文件";
+ return responseData;
+ }
+ // PluginCore.AspNetCore-v1.0.2 起 不再限制插件上传大小
+ //判断文件大小
+ //var fileSize = file.Length;
+ //if (fileSize > 1024 * 1024 * 5) // 5M
+ //{
+ // responseData.Code = -1;
+ // responseData.Message = "上传的文件不能大于5MB";
+ // return responseData;
+ //}
+ #endregion
+
+ try
+ {
+ // 1.先上传到 临时插件上传目录, 用Guid.zip作为保存文件名
+ string tempZipFilePath = Path.Combine(PluginPathProvider.TempPluginUploadDir(), Guid.NewGuid() + ".zip");
+ using (var fs = System.IO.File.Create(tempZipFilePath))
+ {
+ file.CopyTo(fs); //将上传的文件文件流,复制到fs中
+ fs.Flush();//清空文件流
+ }
+ // 2.解压
+ bool isDecomparessSuccess = false;
+ if (uploadFileType == UploadFileType.Zip)
+ {
+ isDecomparessSuccess = Utils.ZipHelper.DecomparessFile(tempZipFilePath, tempZipFilePath.Replace(".zip", ""));
+ }
+ else if (uploadFileType == UploadFileType.Nupkg)
+ {
+ isDecomparessSuccess = NupkgService.DecomparessFile(tempZipFilePath, tempZipFilePath.Replace(".zip", ""));
+ }
+
+ // 3.删除原压缩包
+ System.IO.File.Delete(tempZipFilePath);
+ if (!isDecomparessSuccess)
+ {
+ responseData.Code = -1;
+ responseData.Message = "解压插件压缩包失败";
+ return responseData;
+ }
+ // 4.读取其中的info.json, 获取 PluginId 值
+ PluginInfoModel pluginInfoModel = PluginInfoModelFactory.ReadPluginDir(tempZipFilePath.Replace(".zip", ""));
+ if (pluginInfoModel == null || string.IsNullOrEmpty(pluginInfoModel.PluginId))
+ {
+ // 记得删除已不再需要的临时插件文件夹
+ Directory.Delete(tempZipFilePath.Replace(".zip", ""), true);
+
+ responseData.Code = -1;
+ responseData.Message = "不合法的插件";
+ return responseData;
+ }
+ string pluginId = pluginInfoModel.PluginId;
+ // 5.检索 此 PluginId 是否本地插件已存在
+ var pluginConfigModel = PluginConfigModelFactory.Create();
+ // 本地已经存在的 PluginId
+ IList localExistPluginIds = PluginPathProvider.AllPluginFolderName();
+ if (localExistPluginIds.Contains(pluginId))
+ {
+ // 记得删除已不再需要的临时插件文件夹
+ Directory.Delete(tempZipFilePath.Replace(".zip", ""), true);
+
+ responseData.Code = -1;
+ responseData.Message = $"本地已有此插件 (PluginId: {pluginId}), 请前往插件列表删除后, 再上传";
+ return responseData;
+ }
+ // 6.本地无此插件 -> 移动插件文件夹到 Plugins 下, 并以 PluginId 为插件文件夹名
+ string pluginsRootPath = PluginPathProvider.PluginsRootPath();
+ string newPluginDir = Path.Combine(pluginsRootPath, pluginId);
+ Directory.Move(tempZipFilePath.Replace(".zip", ""), newPluginDir);
+
+ // 7. 放入 Plugins 中, 默认为 已禁用
+
+ responseData.Code = 1;
+ responseData.Message = $"上传插件成功 (PluginId: {pluginId})";
+ }
+ catch (Exception ex)
+ {
+ responseData.Code = -1;
+ responseData.Message = "上传插件失败: " + ex.Message;
+ ex = ex.InnerException;
+ while (ex != null)
+ {
+ responseData.Message += " - " + ex.InnerException.Message;
+ ex = ex.InnerException;
+ }
+ Utils.LogUtil.Error(ex, ex.Message);
+ }
+
+ return await Task.FromResult(responseData);
+ }
+ #endregion
+
+ #region 查看详细
+ [HttpGet, HttpPost]
+ public async Task> Details(string pluginId)
+ {
+ BaseResponseModel responseData = new BaseResponseModel();
+
+ try
+ {
+ #region 效验
+ pluginId = pluginId.Trim();
+ var pluginConfigModel = PluginConfigModelFactory.Create();
+ string[] localPluginIds = PluginPathProvider.AllPluginFolderName().ToArray();
+
+ if (!localPluginIds.Contains(pluginId))
+ {
+ responseData.Code = -1;
+ responseData.Message = $"查看详细失败: 不存在 {pluginId} 插件";
+ return await Task.FromResult(responseData);
+ }
+
+ #endregion
+
+ PluginInfoModel pluginInfoModel = PluginInfoModelFactory.Create(pluginId);
+ string[] enablePluginIds = _pluginFinder.EnablePluginIds().ToArray();
+ PluginInfoResponseModel pluginInfoResponseModel = PluginInfoModelToResponseModel(new List() { pluginInfoModel }, pluginConfigModel, enablePluginIds).FirstOrDefault();
+
+
+ responseData.Code = 1;
+ responseData.Message = "查看详细成功";
+ responseData.Data = pluginInfoResponseModel;
+ }
+ catch (Exception ex)
+ {
+ responseData.Code = -1;
+ responseData.Message = "查看详细失败: " + ex.Message;
+ }
+
+ return await Task.FromResult(responseData);
+ }
+ #endregion
+
+ #region 查看文档
+ [HttpGet, HttpPost]
+ public async Task> Readme(string pluginId)
+ {
+ BaseResponseModel responseData = new BaseResponseModel();
+
+ try
+ {
+ #region 效验
+ pluginId = pluginId.Trim();
+ // var pluginConfigModel = PluginConfigModelFactory.Create();
+ string[] localPluginIds = PluginPathProvider.AllPluginFolderName().ToArray();
+
+ if (!localPluginIds.Contains(pluginId))
+ {
+ responseData.Code = -1;
+ responseData.Message = $"查看文档失败: 不存在 {pluginId} 插件";
+ return await Task.FromResult(responseData);
+ }
+
+ #endregion
+
+ PluginReadmeModel readmeModel = PluginReadmeModelFactory.Create(pluginId);
+ PluginReadmeResponseModel readmeResponseModel = new PluginReadmeResponseModel();
+ readmeResponseModel.Content = readmeModel?.Content ?? "";
+ readmeResponseModel.PluginId = pluginId;
+
+ responseData.Code = 1;
+ responseData.Message = "查看文档成功";
+ responseData.Data = readmeResponseModel;
+ }
+ catch (Exception ex)
+ {
+ responseData.Code = -1;
+ responseData.Message = "查看文档失败: " + ex.Message;
+ }
+
+ return await Task.FromResult(responseData);
+ }
+ #endregion
+
+ #region 设置
+ [HttpGet]
+ public async Task> Settings(string pluginId)
+ {
+ BaseResponseModel responseData = new BaseResponseModel();
+
+ try
+ {
+ #region 效验
+ pluginId = pluginId.Trim();
+ // var pluginConfigModel = PluginConfigModelFactory.Create();
+ string[] localPluginIds = PluginPathProvider.AllPluginFolderName().ToArray();
+
+ if (!localPluginIds.Contains(pluginId))
+ {
+ responseData.Code = -1;
+ responseData.Message = $"查看设置失败: 不存在 {pluginId} 插件";
+ return await Task.FromResult(responseData);
+ }
+
+ #endregion
+
+ string settingsJsonStr = PluginSettingsModelFactory.Create(pluginId);
+
+
+ responseData.Code = 1;
+ responseData.Message = "查看设置成功";
+ responseData.Data = settingsJsonStr ?? "无设置项";
+ }
+ catch (Exception ex)
+ {
+ responseData.Code = -1;
+ responseData.Message = "查看设置失败: " + ex.Message;
+ }
+
+ return await Task.FromResult(responseData);
+ }
+
+ [HttpPost]
+ public async Task> Settings(PluginSettingsInputModel inputModel)
+ {
+ BaseResponseModel responseData = new BaseResponseModel();
+
+ try
+ {
+ #region 效验
+ inputModel.PluginId = inputModel.PluginId.Trim();
+ // var pluginConfigModel = PluginConfigModelFactory.Create();
+ string[] localPluginIds = PluginPathProvider.AllPluginFolderName().ToArray();
+
+ if (!localPluginIds.Contains(inputModel.PluginId))
+ {
+ responseData.Code = -1;
+ responseData.Message = $"设置失败: 不存在 {inputModel.PluginId} 插件";
+ return await Task.FromResult(responseData);
+ }
+
+ #endregion
+
+ inputModel.Data = inputModel.Data ?? "";
+ PluginSettingsModelFactory.Save(pluginSettingsJsonStr: inputModel.Data, pluginId: inputModel.PluginId);
+
+
+ responseData.Code = 1;
+ responseData.Message = "设置成功";
+ responseData.Data = inputModel.Data;
+ }
+ catch (Exception ex)
+ {
+ responseData.Code = -1;
+ responseData.Message = "设置失败: " + ex.Message;
+ }
+
+ return await Task.FromResult(responseData);
+ }
+ #endregion
+
+ #endregion
+
+
+ #region Helpers
+
+ [NonAction]
+ private IList PluginInfoModelToResponseModel(IList pluginInfoModels, PluginConfigModel pluginConfigModel, string[] enablePluginIds)
+ {
+ // 获取 Plugins 下所有插件
+ // DirectoryInfo pluginsDir = new DirectoryInfo(PluginPathProvider.PluginsRootPath());
+ // List pluginIds = pluginsDir?.GetDirectories()?.Select(m => m.Name)?.ToList() ?? new List();
+
+ IList responseModels = new List();
+ #region 添加插件状态信息
+ foreach (var model in pluginInfoModels)
+ {
+ PluginInfoResponseModel responseModel = new PluginInfoResponseModel();
+ responseModel.Author = model.Author;
+ responseModel.Description = model.Description;
+ responseModel.DisplayName = model.DisplayName;
+ responseModel.PluginId = model.PluginId;
+ responseModel.SupportedVersions = model.SupportedVersions;
+ responseModel.Version = model.Version;
+ responseModel.DependPlugins = model.DependPlugins;
+
+ if (pluginConfigModel.EnabledPlugins.Contains(model.PluginId) && !enablePluginIds.Contains(model.PluginId)) {
+ // 错误情况: 配置 标识 已启用, 但实际没有启用成功
+ pluginConfigModel.EnabledPlugins.Remove(model.PluginId);
+ PluginConfigModelFactory.Save(pluginConfigModel);
+
+ responseModel.Status = PluginStatus.Disabled;
+ } else if(!pluginConfigModel.EnabledPlugins.Contains(model.PluginId) && enablePluginIds.Contains(model.PluginId))
+ {
+ // 错误情况: 配置没有标识 已启用, 但实际 已启用
+ pluginConfigModel.EnabledPlugins.Add(model.PluginId);
+ PluginConfigModelFactory.Save(pluginConfigModel);
+
+ responseModel.Status = PluginStatus.Enabled;
+ } else if (pluginConfigModel.EnabledPlugins.Contains(model.PluginId) && enablePluginIds.Contains(model.PluginId))
+ {
+ responseModel.Status = PluginStatus.Enabled;
+ }
+ else
+ {
+ responseModel.Status = PluginStatus.Disabled;
+ }
+ responseModels.Add(responseModel);
+ }
+ #endregion
+
+ return responseModels;
+ }
+
+ public enum UploadFileType
+ {
+ NoAllowedType = 0,
+ Zip = 1,
+ Nupkg = 2
+ }
+
+ #endregion
+ }
+}
+
+
diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/UserController.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/UserController.cs
new file mode 100644
index 00000000..fa6193ed
--- /dev/null
+++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Controllers忽略/UserController.cs
@@ -0,0 +1,143 @@
+//===================================================
+// License: GNU LGPLv3
+// Contributors: yiyungent@gmail.com
+// Project: https://yiyungent.github.io/PluginCore
+// GitHub: https://github.com/yiyungent/PluginCore
+//===================================================
+
+
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc;
+using PluginCore.AspNetCore.Authorization;
+using PluginCore.Config;
+using PluginCore.AspNetCore.RequestModel.User;
+using PluginCore.AspNetCore.ResponseModel;
+
+namespace PluginCore.AspNetCore.Controllers
+{
+ [Route("api/plugincore/admin/[controller]/[action]")]
+ [ApiController]
+ public class UserController : ControllerBase
+ {
+ private readonly AccountManager _accountManager;
+
+ public string RemoteFronted
+ {
+ get
+ {
+ return PluginCore.Config.PluginCoreConfigFactory.Create().RemoteFrontend;
+ }
+ }
+
+ public UserController(AccountManager accountManager)
+ {
+ _accountManager = accountManager;
+ }
+
+ [HttpGet, HttpPost]
+ public async Task> Login([FromBody] LoginRequestModel requestModel)
+ {
+ BaseResponseModel responseModel = new BaseResponseModel();
+
+ try
+ {
+ string token = AccountManager.CreateToken(requestModel.UserName, requestModel.Password);
+ bool isAdmin = AccountManager.IsAdminToken(token);
+ if (!isAdmin)
+ {
+ responseModel.Code = -1;
+ responseModel.Message = "用户名或密码不正确";
+
+ return await Task.FromResult(responseModel);
+ }
+
+ responseModel.Code = 1;
+ responseModel.Message = "登录成功";
+ responseModel.Data = new
+ {
+ token = token,
+ userName = requestModel.UserName
+ };
+ }
+ catch (Exception ex)
+ {
+ responseModel.Code = -1;
+ responseModel.Message = "失败: " + ex.Message;
+ }
+
+ return await Task.FromResult(responseModel);
+ }
+
+ [HttpGet, HttpPost]
+ public async Task> Logout()
+ {
+ BaseResponseModel responseModel = new BaseResponseModel()
+ {
+ Code = 1,
+ Message = "退出登录成功"
+ };
+
+ return await Task.FromResult(responseModel);
+ }
+
+ [PluginCoreAdminAuthorize]
+ [HttpGet, HttpPost]
+ public async Task> Info()
+ {
+ BaseResponseModel responseModel = new BaseResponseModel();
+
+ try
+ {
+ string adminUserName = PluginCoreConfigFactory.Create().Admin.UserName;
+
+ responseModel.Code = 1;
+ responseModel.Message = "成功";
+ responseModel.Data = new
+ {
+ name = adminUserName,
+ //avatar = this.RemoteFronted + "/images/avatar.gif"
+ avatar = ""
+ };
+ }
+ catch (Exception ex)
+ {
+ responseModel.Code = -1;
+ responseModel.Message = "失败: " + ex.Message;
+ }
+
+ return await Task.FromResult(responseModel);
+ }
+
+ [PluginCoreAdminAuthorize]
+ [HttpGet, HttpPost]
+ public async Task> Update([FromBody] UpdateRequestModel requestModel)
+ {
+ BaseResponseModel responseModel = new BaseResponseModel();
+
+ try
+ {
+ PluginCoreConfig pluginCoreConfig = PluginCoreConfigFactory.Create();
+ pluginCoreConfig.Admin.UserName = requestModel.UserName;
+ pluginCoreConfig.Admin.Password = requestModel.Password;
+ PluginCoreConfigFactory.Save(pluginCoreConfig);
+
+ responseModel.Code = 1;
+ responseModel.Message = "修改成功, 需要重新登录";
+ }
+ catch (Exception ex)
+ {
+ responseModel.Code = -1;
+ responseModel.Message = "失败: " + ex.Message;
+ }
+
+ return await Task.FromResult(responseModel);
+ }
+
+ }
+}
+
+
diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Extensions/IServiceProviderExtensions.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Extensions/IServiceProviderExtensions.cs
new file mode 100644
index 00000000..e75d13c8
--- /dev/null
+++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Extensions/IServiceProviderExtensions.cs
@@ -0,0 +1,152 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Text;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace PluginCore.AspNetCore.Extensions
+{
+ public static class IServiceProviderExtensions
+ {
+ ///
+ /// Get all registered
+ ///
+ ///
+ ///
+ public static Dictionary GetAllServiceDescriptors(this IServiceProvider provider)
+ {
+ var serviceProvider = provider.GetPropertyValue("RootProvider");
+ var result = new Dictionary();
+
+ var engine = serviceProvider.GetFieldValue("_engine");
+ // var callSiteFactory = engine.GetPropertyValue("CallSiteFactory");
+ var callSiteFactory = serviceProvider.GetPropertyValue("CallSiteFactory");
+ var descriptorLookup = callSiteFactory.GetFieldValue("_descriptorLookup");
+ if (descriptorLookup is IDictionary dictionary)
+ {
+ foreach (DictionaryEntry entry in dictionary)
+ {
+ result.Add((Type)entry.Key, (ServiceDescriptor)entry.Value.GetPropertyValue("Last"));
+ }
+ }
+
+ return result;
+
+ #region Old
+ //if (provider is ServiceProvider serviceProvider)
+ //{
+ // var result = new Dictionary();
+
+ // var engine = serviceProvider.GetFieldValue("_engine");
+ // var callSiteFactory = engine.GetPropertyValue("CallSiteFactory");
+ // var descriptorLookup = callSiteFactory.GetFieldValue("_descriptorLookup");
+ // if (descriptorLookup is IDictionary dictionary)
+ // {
+ // foreach (DictionaryEntry entry in dictionary)
+ // {
+ // result.Add((Type)entry.Key, (ServiceDescriptor)entry.Value.GetPropertyValue("Last"));
+ // }
+ // }
+
+ // return result;
+ //}
+
+ //throw new NotSupportedException($"Type '{provider.GetType()}' is not supported!");
+ #endregion
+ }
+ }
+
+ public static class ReflectionHelper
+ {
+ // ##########################################################################################
+ // Get / Set Field
+ // ##########################################################################################
+
+ #region Get / Set Field
+
+ public static object GetFieldValue(this object obj, string fieldName)
+ {
+ if (obj == null)
+ throw new ArgumentNullException(nameof(obj));
+ Type objType = obj.GetType();
+ var fieldInfo = GetFieldInfo(objType, fieldName);
+ if (fieldInfo == null)
+ throw new ArgumentOutOfRangeException(fieldName,
+ $"Couldn't find field {fieldName} in type {objType.FullName}");
+ return fieldInfo.GetValue(obj);
+ }
+
+ public static void SetFieldValue(this object obj, string fieldName, object val)
+ {
+ if (obj == null)
+ throw new ArgumentNullException(nameof(obj));
+ Type objType = obj.GetType();
+ var fieldInfo = GetFieldInfo(objType, fieldName);
+ if (fieldInfo == null)
+ throw new ArgumentOutOfRangeException(fieldName,
+ $"Couldn't find field {fieldName} in type {objType.FullName}");
+ fieldInfo.SetValue(obj, val);
+ }
+
+ private static FieldInfo GetFieldInfo(Type type, string fieldName)
+ {
+ FieldInfo fieldInfo = null;
+ do
+ {
+ fieldInfo = type.GetField(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
+ type = type.BaseType;
+ } while (fieldInfo == null && type != null);
+
+ return fieldInfo;
+ }
+
+ #endregion
+
+ // ##########################################################################################
+ // Get / Set Property
+ // ##########################################################################################
+
+ #region Get / Set Property
+
+ public static object GetPropertyValue(this object obj, string propertyName)
+ {
+ if (obj == null)
+ throw new ArgumentNullException(nameof(obj));
+ Type objType = obj.GetType();
+ var propertyInfo = GetPropertyInfo(objType, propertyName);
+ if (propertyInfo == null)
+ throw new ArgumentOutOfRangeException(propertyName,
+ $"Couldn't find property {propertyName} in type {objType.FullName}");
+ return propertyInfo.GetValue(obj, null);
+ }
+
+ public static void SetPropertyValue(this object obj, string propertyName, object val)
+ {
+ if (obj == null)
+ throw new ArgumentNullException(nameof(obj));
+ Type objType = obj.GetType();
+ var propertyInfo = GetPropertyInfo(objType, propertyName);
+ if (propertyInfo == null)
+ throw new ArgumentOutOfRangeException(propertyName,
+ $"Couldn't find property {propertyName} in type {objType.FullName}");
+ propertyInfo.SetValue(obj, val, null);
+ }
+
+ private static PropertyInfo GetPropertyInfo(Type type, string propertyName)
+ {
+ PropertyInfo propertyInfo = null;
+ do
+ {
+ propertyInfo = type.GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
+ type = type.BaseType;
+ } while (propertyInfo == null && type != null);
+
+ return propertyInfo;
+ }
+
+ #endregion
+ }
+}
+
+
diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Extensions/PluginCoreStartupExtensions.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Extensions/PluginCoreStartupExtensions.cs
new file mode 100644
index 00000000..32549a3b
--- /dev/null
+++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Extensions/PluginCoreStartupExtensions.cs
@@ -0,0 +1,416 @@
+//===================================================
+// License: GNU LGPLv3
+// Contributors: yiyungent@gmail.com
+// Project: https://yiyungent.github.io/PluginCore
+// GitHub: https://github.com/yiyungent/PluginCore
+//===================================================
+
+
+
+using PluginCore.Models;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
+using Microsoft.Extensions.DependencyInjection;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.FileProviders;
+using PluginCore.AspNetCore.Authorization;
+using PluginCore.AspNetCore.AdminUI;
+using PluginCore.Infrastructure;
+using PluginCore.Interfaces;
+using PluginCore.AspNetCore.Middlewares;
+using PluginCore.AspNetCore.BackgroundServices;
+using PluginCore.IPlugins;
+using PluginCore.AspNetCore.Interfaces;
+using PluginCore.lmplements;
+using PluginCore.AspNetCore.lmplements;
+
+namespace PluginCore.AspNetCore.Extensions
+{
+ ///
+ ///
+ ///
+ public static class PluginCoreStartupExtensions
+ {
+ private static IWebHostEnvironment _webHostEnvironment;
+
+ private static IServiceCollection _services;
+
+ private static IServiceProvider _serviceProvider;
+
+ ///
+ /// 若需要替换默认实现, 请在 之前, 若在之后, 则无法影响 主程序启动时默认行为
+ ///
+ ///
+ public static void AddPluginCore(this IServiceCollection services)
+ {
+ services.AddPluginCoreServices();
+
+ services.AddPluginCoreLog();
+
+ services.AddPluginCorePlugins();
+
+ #region PluginCore Admin 权限
+ // 1. 先 Authentication (我是谁) 2. 再 Authorization (我能做什么)
+
+ // Authentication
+ //注销内置认证 by tomny
+ // services.AddPluginCoreAuthentication();
+ //注销内置授权 by tomny
+ // Authorization
+ //services.AddPluginCoreAuthorization();
+ #endregion
+
+ services.AddPluginCoreStartupPlugin();
+
+ // AddBackgroundServices
+ services.AddBackgroundServices();
+
+ // 一定要在最后
+ _services = services;
+ _serviceProvider = services.BuildServiceProvider();
+ }
+
+ public static IApplicationBuilder UsePluginCore(this IApplicationBuilder app)
+ {
+ // 一定在 PluginCore 添加的中间件中 第一个
+ app.UseMiddleware();
+
+ app.UsePluginCoreLanguageMiddleware();
+ //注销内置管理页面 by tomny
+ // app.UsePluginCoreAdminUI();
+
+ app.UsePluginCoreStaticFiles();
+
+ // 发现 UseAuthentication 认证中间件重复添加, 也只会执行一次 认证
+ // 但 UseAuthorization 重复添加2次, 则会执行 2次 授权
+ //注销内置认证授权 by tomny
+ //app.UseAuthentication();
+ //app.UseAuthorization();
+
+ #region Plugin Middleware
+ // Plugin Middleware
+ //app.UseMiddleware();
+
+
+ // 一定在 PluginCore 添加的中间件中 最后一个
+ app.UseMiddleware();
+ #endregion
+
+ app.UsePluginCoreStartupPlugin();
+
+ app.UsePluginCoreAppStart();
+
+ // Log
+ app.UsePluginCoreLog();
+
+ return app;
+ }
+
+
+ public static void AddPluginCoreServices(this IServiceCollection services)
+ {
+ #region 注册服务
+
+ #region 仅适用于 ASP.NET Core
+ // start: 仅用于 ASP.NET Core
+ // 用于添加插件Controller 时,通知Controller.Action发生变化
+ services.AddSingleton(PluginActionDescriptorChangeProvider.Instance);
+ services.AddSingleton(PluginActionDescriptorChangeProvider.Instance);
+
+ services.TryAddTransient();
+ services.TryAddTransient();
+
+ services.TryAddTransient();
+ services.TryAddTransient();
+ // end: 仅用于 ASP.NET Core
+ #endregion
+
+ #region 通用
+
+ // v1 旧版
+ //services.TryAddTransient();
+ //services.TryAddTransient();
+ //services.TryAddTransient();
+ //services.TryAddTransient();
+
+ services.TryAddTransient();
+ services.TryAddTransient();
+ //注销内置插件管理 by tomny
+ services.TryAddTransient();
+ services.TryAddTransient();
+
+ services.TryAddTransient();
+ services.TryAddTransient();
+ services.TryAddTransient();
+ services.TryAddTransient();
+
+ // 注意: 它必须单例
+ // TODO: 不知道原因, 单例失败, 每次 获取 IPluginFinder 都会获取新的 IPluginContextManager
+ services.TryAddSingleton();
+ services.TryAddSingleton();
+ #endregion
+
+ #endregion
+
+ // ************************* 注意: IServiceCollection 是服务列表, 但由 IServiceProvider 来负责解析, AClass 单例 仅在 AServiceProvider 范围内
+ _serviceProvider = services.BuildServiceProvider();
+ }
+
+ public static void AddPluginCorePlugins(this IServiceCollection services)
+ {
+ #region ASP.NET Core
+ //IPluginManager pluginManager = services.BuildServiceProvider().GetService();
+ IPluginManager pluginManager = _serviceProvider.GetService();
+
+ // 初始化 PluginCore 相关目录
+ PluginPathProvider.PluginsRootPath();
+
+ // 在程序启动时加载所有 已安装并启用 的插件
+
+ // 获取 PluginConfigModel
+ #region 获取 PluginConfigModel
+ PluginConfigModel pluginConfigModel = PluginConfigModelFactory.Create();
+ #endregion
+
+ // 已启用的插件
+ #region 加载 已启用插件的Assemblies
+ IList enabledPluginIds = pluginConfigModel.EnabledPlugins;
+ foreach (var pluginId in enabledPluginIds)
+ {
+ pluginManager.LoadPlugin(pluginId);
+ }
+ #endregion
+
+ //注销内置Controller的注册 by tomny
+ // 将本 Assembly 内的 Controller 添加
+ // var ass = Assembly.GetExecutingAssembly();
+ //IPluginControllerManager pluginControllerManager = services.BuildServiceProvider().GetService();
+ // IPluginControllerManager pluginControllerManager = _serviceProvider.GetService();
+ //pluginControllerManager.AddControllers(ass);
+
+
+ // IWebHostEnvironment
+ _webHostEnvironment = services.BuildServiceProvider().GetService();
+ #endregion
+ }
+
+ public static void AddPluginCoreAuthentication(this IServiceCollection services)
+ {
+ // fixed: https://github.com/yiyungent/PluginCore/issues/4
+ // System.InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action configureOptions).
+ #region 添加认证 Authentication
+ // 没通过 Authentication: 401 Unauthorized
+ // services.AddAuthentication("PluginCore.Authentication")
+ // .AddScheme("PluginCore.Authentication", "PluginCore.Authentication",
+ // options => { });
+ // 注意: 不要设置 默认 认证名: Constants.AspNetCoreAuthenticationScheme
+ // services.AddAuthentication(Constants.AspNetCoreAuthenticationScheme)
+ // 默认认证名: 默认
+ services.AddAuthentication()
+ // 添加一个新的认证方案
+ .AddScheme(
+ authenticationScheme: Constants.AspNetCoreAuthenticationScheme,
+ displayName: Constants.AspNetCoreAuthenticationScheme,
+ options => { });
+ #endregion
+ }
+
+ public static void AddPluginCoreAuthorization(this IServiceCollection services)
+ {
+ #region 添加授权策略-所有标记 [PluginCoreAdminAuthorize] 都需要权限检查
+ // Only Once, Not recommend
+ //services.AddSingleton();
+
+ services.AddAuthorization(options =>
+ {
+ // options.AddPolicy("PluginCore.Admin", policy =>
+ options.AddPolicy(name: Constants.AspNetCoreAuthorizationPolicyName, policy =>
+ {
+ // 无法满足 下方任何一项:HTTP 403 错误
+ // 3.需要 检查是否拥有当前请求资源的权限
+ //policy.Requirements.Add(new PluginCoreAdminRequirement());
+ policy.AuthenticationSchemes = new string[] {
+ Constants.AspNetCoreAuthenticationScheme
+ };
+ policy.RequireAuthenticatedUser();
+ policy.RequireClaim(claimType: Constants.AspNetCoreAuthenticationClaimType);
+ // 必须重启才能使得更改密码生效
+ string token = AccountManager.CreateToken();
+ policy.RequireClaim(claimType: Constants.AspNetCoreAuthenticationClaimType, allowedValues: new string[] {
+ token
+ });
+ //policy.RequireAssertion(context =>
+ //{
+ // return true;
+ //});
+ });
+ });
+ #endregion
+
+ // AccountManager
+ services.AddTransient();
+ // HttpContext.Current
+ services.TryAddSingleton();
+ //services.AddHttpContextAccessor();
+ }
+
+ public static void AddPluginCoreStartupPlugin(this IServiceCollection services)
+ {
+ //IPluginFinder pluginFinder = services.BuildServiceProvider().GetService();
+ IPluginFinder pluginFinder = _serviceProvider.GetService();
+
+ #region IStartupPlugin
+
+ var plugins = pluginFinder.EnablePlugins()?.OrderBy(m => m.ConfigureServicesOrder)?.ToList();
+
+ foreach (var item in plugins)
+ {
+ // 调用
+ Utils.LogUtil.Info(
+ categoryName: $"{nameof(PluginCoreStartupExtensions)}.{nameof(AddPluginCoreStartupPlugin)}",
+ message: $"{item.GetType().ToString()}: {nameof(IStartupPlugin)}.{nameof(IStartupPlugin.ConfigureServices)}"
+ );
+
+ item?.ConfigureServices(services);
+ }
+
+ #endregion
+ }
+
+ public static void AddPluginCoreLog(this IServiceCollection services)
+ {
+ #region Logger
+
+ IServiceScopeFactory serviceScopeFactory = _serviceProvider.GetService();
+ Utils.LogUtil.Initialize(serviceScopeFactory);
+
+ #endregion
+ }
+
+ public static IApplicationBuilder UsePluginCoreStaticFiles(this IApplicationBuilder app)
+ {
+ // TODO: 其实由于目前已实现运行时动态新增/删除 HTTP Middleware, 其实可以不用再像下方这么复制插件 wwwroot 目录到 Plugins_wwwroot/{PluginId}
+ // 而是在运行时配置, 直接指向 `Plugins/{PluginId}/wwwroot`, 而无需复制/删除
+
+ // 注意:`UseDefaultFiles`必须在`UseStaticFiles`之前进行调用。因为`DefaultFilesMiddleware`仅仅负责重写Url,实际上默认页文件,仍然是通过`StaticFilesMiddleware`来提供的。
+
+ string pluginwwwrootDir = PluginPathProvider.PluginsWwwRootDir();
+
+ #region 插件 wwwroot 默认页
+ // 设置目录的默认页
+ var defaultFilesOptions = new DefaultFilesOptions();
+ defaultFilesOptions.DefaultFileNames.Clear();
+ // 指定默认页名称
+ defaultFilesOptions.DefaultFileNames.Add("index.html");
+ // 指定请求路径
+ defaultFilesOptions.RequestPath = "/Plugins";
+ // 指定默认页所在的目录
+ defaultFilesOptions.FileProvider = new PhysicalFileProvider(pluginwwwrootDir);
+ app.UseDefaultFiles(defaultFilesOptions);
+ #endregion
+
+ #region 插件 wwwroot
+ // 由于没办法在运行时, 动态 UseStaticFiles(), 因此不再为每一个插件都 UseStaticFiles(),
+ // 而是统一在一个文件夹下, 插件启用时, 将插件的wwwroot复制到 Plugins_wwwroot/{PluginId}, 禁用时, 再删除
+ app.UseStaticFiles(new StaticFileOptions
+ {
+ FileProvider = new PhysicalFileProvider(
+ pluginwwwrootDir),
+ RequestPath = "/Plugins"
+ });
+ #endregion
+
+ return app;
+ }
+
+ public static IApplicationBuilder UsePluginCoreAppStart(this IApplicationBuilder app)
+ {
+ //IPluginFinder pluginFinder = _services.BuildServiceProvider().GetService();
+ IPluginFinder pluginFinder = _serviceProvider.GetService();
+
+ #region AppStart
+
+ var plugins = pluginFinder.EnablePluginsFull()?.ToList();
+ var dependencySorter = new PluginCore.Utils.DependencySorter();
+ dependencySorter.AddObjects(plugins.Select(m => m.PluginInstance).ToArray());
+ foreach (var item in plugins)
+ {
+ var dependPlugins = plugins.Where(m => item.PluginInstance.AppStartOrderDependPlugins.Contains(m.PluginId)).Select(m => m.PluginInstance).ToArray();
+ dependencySorter.SetDependencies(obj: item.PluginInstance, dependsOnObjects: dependPlugins);
+ }
+ var sortedPlugins = dependencySorter.Sort();
+ foreach (var item in sortedPlugins)
+ {
+ // 调用
+ //Utils.LogUtil.PluginBehavior(item, typeof(IPlugin), nameof(IPlugin.AppStart));
+ Utils.LogUtil.Info(
+ categoryName: $"{nameof(PluginCoreStartupExtensions)}.{nameof(UsePluginCoreAppStart)}",
+ message: $"{item.GetType().ToString()}: {nameof(IPlugin)}.{nameof(IPlugin.AppStart)}"
+ );
+
+ item?.AppStart();
+ }
+
+ #endregion
+
+ return app;
+ }
+
+ public static IApplicationBuilder UsePluginCoreStartupPlugin(this IApplicationBuilder app)
+ {
+ //IPluginFinder pluginFinder = _services.BuildServiceProvider().GetService();
+ IPluginFinder pluginFinder = _serviceProvider.GetService();
+
+ #region IStartupPlugin
+
+ var startupPlugins = pluginFinder.EnablePlugins()?.OrderBy(m => m.ConfigureOrder)?.ToList();
+
+ foreach (var item in startupPlugins)
+ {
+ // 调用
+ //Utils.LogUtil.PluginBehavior(item, typeof(IStartupPlugin), nameof(IStartupPlugin.Configure));
+ Utils.LogUtil.Info(
+ categoryName: $"{nameof(PluginCoreStartupExtensions)}.{nameof(UsePluginCoreStartupPlugin)}",
+ message: $"{item.GetType().ToString()}: {nameof(IStartupPlugin)}.{nameof(IStartupPlugin.Configure)}"
+ );
+
+ item?.Configure(app);
+ }
+
+ #endregion
+
+
+ app.UseMiddleware();
+
+ return app;
+ }
+
+ public static IApplicationBuilder UsePluginCoreLog(this IApplicationBuilder app)
+ {
+ #region 启动 Log
+ Config.PluginCoreConfig pluginCoreConfig = Config.PluginCoreConfigFactory.Create();
+
+ Utils.LogUtil.Info(categoryName: $"{nameof(PluginCoreStartupExtensions)}.{nameof(UsePluginCoreLog)}", $"{nameof(PluginCore.AspNetCore)}");
+ Utils.LogUtil.Info(categoryName: $"{nameof(PluginCoreStartupExtensions)}.{nameof(UsePluginCoreLog)}", "Started successfully:");
+ Utils.LogUtil.Info(categoryName: $"{nameof(PluginCoreStartupExtensions)}.{nameof(UsePluginCoreLog)}", $"Front-end mode: {pluginCoreConfig.FrontendMode}");
+ Utils.LogUtil.Info(categoryName: $"{nameof(PluginCoreStartupExtensions)}.{nameof(UsePluginCoreLog)}", $"Notice: Updating the front-end mode requires restarting the site");
+ #endregion
+
+ return app;
+ }
+ }
+}
+
+
diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Infrastructure/PluginApplicationBuilder.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Infrastructure/PluginApplicationBuilder.cs
new file mode 100644
index 00000000..01ea979d
--- /dev/null
+++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Infrastructure/PluginApplicationBuilder.cs
@@ -0,0 +1,350 @@
+//===================================================
+// License: GNU LGPLv3
+// Contributors: yiyungent@gmail.com
+// Project: https://yiyungent.github.io/PluginCore
+// GitHub: https://github.com/yiyungent/PluginCore
+//===================================================
+
+
+
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Features;
+using Microsoft.Extensions.Internal;
+
+namespace PluginCore.AspNetCore.Infrastructure
+{
+ ///
+ /// Default implementation for .
+ ///
+ /// https://github.com/dotnet/aspnetcore/blob/main/src/Http/Http/src/Builder/ApplicationBuilder.cs
+ ///
+ public class PluginApplicationBuilder : IApplicationBuilder
+ {
+ private const string ServerFeaturesKey = "server.Features";
+ private const string ApplicationServicesKey = "application.Services";
+
+ private readonly List> _components = new List>();
+
+ private Action _reachEndAction;
+
+ public Action ReachEndAction
+ {
+ get
+ {
+ return this._reachEndAction;
+ }
+ set
+ {
+ this._reachEndAction = value;
+ }
+ }
+
+ public PluginApplicationBuilder()
+ {
+
+ }
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The for application services.
+ public PluginApplicationBuilder(IServiceProvider serviceProvider, Action reachEndAction)
+ {
+ Properties = new Dictionary(StringComparer.Ordinal);
+ ApplicationServices = serviceProvider;
+
+ _reachEndAction = reachEndAction;
+ }
+
+ ///
+ /// Initializes a new instance of .
+ ///
+ /// The for application services.
+ /// The server instance that hosts the application.
+ public PluginApplicationBuilder(IServiceProvider serviceProvider, Action reachEndAction, object server)
+ : this(serviceProvider, reachEndAction)
+ {
+ SetProperty(ServerFeaturesKey, server);
+ }
+
+ private PluginApplicationBuilder(PluginApplicationBuilder builder)
+ {
+ Properties = new CopyOnWriteDictionary(builder.Properties, StringComparer.Ordinal);
+ }
+
+ ///
+ /// Gets the for application services.
+ ///
+ public IServiceProvider ApplicationServices
+ {
+ get
+ {
+ //return GetProperty(ApplicationServicesKey)!;
+ return null;
+ }
+ set
+ {
+ SetProperty(ApplicationServicesKey, value);
+ }
+ }
+
+ ///
+ /// Gets the for server features.
+ ///
+ public IFeatureCollection ServerFeatures
+ {
+ get
+ {
+ // ! (null 包容)运算符(C# 参考)
+ // https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/operators/null-forgiving
+ // TODO: 报错
+ //var result = GetProperty(ServerFeaturesKey)!;
+
+ IFeatureCollection rtn = null;
+
+ return rtn;
+ }
+ }
+
+ ///
+ /// Gets a set of properties for .
+ ///
+ public IDictionary Properties { get; }
+
+ //private T? GetProperty(string key)
+ //{
+
+ // return Properties.TryGetValue(key, out var value) ? (T?)value : default(T);
+ //}
+
+ private void SetProperty(string key, T value)
+ {
+ Properties[key] = value;
+ }
+
+ ///
+ /// Adds the middleware to the application request pipeline.
+ ///
+ /// The middleware.
+ /// An instance of after the operation has completed.
+ public IApplicationBuilder Use(Func middleware)
+ {
+ _components.Add(middleware);
+ return this;
+ }
+
+ ///
+ /// Creates a copy of this application builder.
+ ///
+ /// The created clone has the same properties as the current instance, but does not copy
+ /// the request pipeline.
+ ///
+ ///
+ /// The cloned instance.
+ public IApplicationBuilder New()
+ {
+ return new PluginApplicationBuilder(this);
+ }
+
+ ///
+ /// Produces a that executes added middlewares.
+ ///
+ /// The .
+ public RequestDelegate Build()
+ {
+ RequestDelegate app = context =>
+ {
+ #region Old
+ // If we reach the end of the pipeline, but we have an endpoint, then something unexpected has happened.
+ // This could happen if user code sets an endpoint, but they forgot to add the UseEndpoint middleware.
+ //var endpoint = context.GetEndpoint();
+ //var endpointRequestDelegate = endpoint?.RequestDelegate;
+ //if (endpointRequestDelegate != null)
+ //{
+ // var message =
+ // $"The request reached the end of the pipeline without executing the endpoint: '{endpoint!.DisplayName}'. " +
+ // $"Please register the EndpointMiddleware using '{nameof(IApplicationBuilder)}.UseEndpoints(...)' if using " +
+ // $"routing.";
+ // throw new InvalidOperationException(message);
+ //}
+ #endregion
+
+ this._reachEndAction();
+
+ //context.Response.StatusCode = StatusCodes.Status404NotFound;
+ return Task.CompletedTask;
+ };
+
+ for (var c = _components.Count - 1; c >= 0; c--)
+ {
+ app = _components[c](app);
+ }
+
+ return app;
+ }
+ }
+
+
+
+ ///
+ /// https://github.com/dotnet/aspnetcore/blob/8b30d862de6c9146f466061d51aa3f1414ee2337/src/Shared/CopyOnWriteDictionary/CopyOnWriteDictionary.cs
+ ///
+ ///
+ ///
+ internal class CopyOnWriteDictionary : IDictionary where TKey : notnull
+ {
+ private readonly IDictionary _sourceDictionary;
+ private readonly IEqualityComparer _comparer;
+ private IDictionary? _innerDictionary;
+
+ public CopyOnWriteDictionary(
+ IDictionary sourceDictionary,
+ IEqualityComparer comparer)
+ {
+ if (sourceDictionary == null)
+ {
+ throw new ArgumentNullException(nameof(sourceDictionary));
+ }
+
+ if (comparer == null)
+ {
+ throw new ArgumentNullException(nameof(comparer));
+ }
+
+ _sourceDictionary = sourceDictionary;
+ _comparer = comparer;
+ }
+
+ private IDictionary ReadDictionary
+ {
+ get
+ {
+ return _innerDictionary ?? _sourceDictionary;
+ }
+ }
+
+ private IDictionary WriteDictionary
+ {
+ get
+ {
+ if (_innerDictionary == null)
+ {
+ _innerDictionary = new Dictionary(_sourceDictionary,
+ _comparer);
+ }
+
+ return _innerDictionary;
+ }
+ }
+
+ public virtual ICollection Keys
+ {
+ get
+ {
+ return ReadDictionary.Keys;
+ }
+ }
+
+ public virtual ICollection Values
+ {
+ get
+ {
+ return ReadDictionary.Values;
+ }
+ }
+
+ public virtual int Count
+ {
+ get
+ {
+ return ReadDictionary.Count;
+ }
+ }
+
+ public virtual bool IsReadOnly
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ public virtual TValue this[TKey key]
+ {
+ get
+ {
+ return ReadDictionary[key];
+ }
+ set
+ {
+ WriteDictionary[key] = value;
+ }
+ }
+
+ public virtual bool ContainsKey(TKey key)
+ {
+ return ReadDictionary.ContainsKey(key);
+ }
+
+ public virtual void Add(TKey key, TValue value)
+ {
+ WriteDictionary.Add(key, value);
+ }
+
+ public virtual bool Remove(TKey key)
+ {
+ return WriteDictionary.Remove(key);
+ }
+
+ public virtual bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value)
+ {
+ return ReadDictionary.TryGetValue(key, out value);
+ }
+
+ public virtual void Add(KeyValuePair item)
+ {
+ WriteDictionary.Add(item);
+ }
+
+ public virtual void Clear()
+ {
+ WriteDictionary.Clear();
+ }
+
+ public virtual bool Contains(KeyValuePair item)
+ {
+ return ReadDictionary.Contains(item);
+ }
+
+ public virtual void CopyTo(KeyValuePair[] array, int arrayIndex)
+ {
+ ReadDictionary.CopyTo(array, arrayIndex);
+ }
+
+ public bool Remove(KeyValuePair item)
+ {
+ return WriteDictionary.Remove(item);
+ }
+
+ public virtual IEnumerator> GetEnumerator()
+ {
+ return ReadDictionary.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+ }
+
+
+
+}
+
+
diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Infrastructure/PluginCoreHostingStartup.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Infrastructure/PluginCoreHostingStartup.cs
new file mode 100644
index 00000000..00cfaea4
--- /dev/null
+++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Infrastructure/PluginCoreHostingStartup.cs
@@ -0,0 +1,62 @@
+//===================================================
+// License: GNU LGPLv3
+// Contributors: yiyungent@gmail.com
+// Project: https://yiyungent.github.io/PluginCore
+// GitHub: https://github.com/yiyungent/PluginCore
+//===================================================
+
+
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.DependencyInjection;
+using PluginCore.AspNetCore.Extensions;
+
+[assembly: HostingStartup(typeof(PluginCore.AspNetCore.Infrastructure.PluginCoreHostingStartup))]
+namespace PluginCore.AspNetCore.Infrastructure
+{
+ ///
+ /// https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/host/platform-specific-configuration?view=aspnetcore-5.0
+ ///
+ public class PluginCoreHostingStartup : IHostingStartup
+ {
+ public PluginCoreHostingStartup()
+ {
+
+ }
+
+ public void Configure(IWebHostBuilder builder)
+ {
+ //builder.ConfigureAppConfiguration(config =>
+ //{
+
+ //});
+
+ // 注意: 无论是通过 Program.cs 中 webBuilder.UseSetting(WebHostDefaults.HostingStartupAssembliesKey, "PluginCore");
+ // 还是 通过环境变量 指定承载启动程序集, 都是先执行 外部的承载启动程序集, 再执行主程序的 Startup.cs, 因此在这时, 有些 service 还没有注册
+
+ // TODO: 不知道, 重复 Add, Use 会导致什么, 没有做防止重复
+ builder.ConfigureServices(services =>
+ {
+ // fixed: https://github.com/yiyungent/PluginCore/issues/1
+ // System.InvalidOperationException: 'Unable to resolve service for type 'Microsoft.AspNetCore.Mvc.ApplicationParts.ApplicationPartManager'
+ // TODO: 不确定, 这样是否可行, 事实上之后主程序还会 Add 一次, 不知道是否会导致存在多个 实例
+ // 失败: 不是一个实例, 导致无法改变 Controller
+ //Microsoft.AspNetCore.Mvc.ApplicationParts.ApplicationPartManager applicationPartManager =
+ // new Microsoft.AspNetCore.Mvc.ApplicationParts.ApplicationPartManager();
+ //services.AddSingleton(applicationPartManager);
+
+ services.AddPluginCore();
+ });
+
+ builder.Configure(app =>
+ {
+ app.UsePluginCore();
+ });
+ }
+ }
+}
+
+
diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Interfaces/IPluginApplicationBuilderManager.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Interfaces/IPluginApplicationBuilderManager.cs
new file mode 100644
index 00000000..97a7b5b9
--- /dev/null
+++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Interfaces/IPluginApplicationBuilderManager.cs
@@ -0,0 +1,25 @@
+//===================================================
+// License: GNU LGPLv3
+// Contributors: yiyungent@gmail.com
+// Project: https://yiyungent.github.io/PluginCore
+// GitHub: https://github.com/yiyungent/PluginCore
+//===================================================
+
+
+
+using Microsoft.AspNetCore.Http;
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace PluginCore.AspNetCore.Interfaces
+{
+ public interface IPluginApplicationBuilderManager
+ {
+ void ReBuild();
+
+ RequestDelegate GetBuildResult();
+ }
+}
+
+
diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Interfaces/IPluginControllerManager.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Interfaces/IPluginControllerManager.cs
new file mode 100644
index 00000000..e89a28ea
--- /dev/null
+++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Interfaces/IPluginControllerManager.cs
@@ -0,0 +1,25 @@
+//===================================================
+// License: GNU LGPLv3
+// Contributors: yiyungent@gmail.com
+// Project: https://yiyungent.github.io/PluginCore
+// GitHub: https://github.com/yiyungent/PluginCore
+//===================================================
+
+
+
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Text;
+
+namespace PluginCore.AspNetCore.Interfaces
+{
+ public interface IPluginControllerManager
+ {
+ void AddControllers(Assembly assembly);
+
+ void RemoveControllers(string pluginId);
+ }
+}
+
+
diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Middlewares/LanguageMiddleware.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Middlewares/LanguageMiddleware.cs
new file mode 100644
index 00000000..beb7bdae
--- /dev/null
+++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Middlewares/LanguageMiddleware.cs
@@ -0,0 +1,47 @@
+//===================================================
+// License: GNU LGPLv3
+// Contributors: yiyungent@gmail.com
+// Project: https://yiyungent.github.io/PluginCore
+// GitHub: https://github.com/yiyungent/PluginCore
+//===================================================
+
+
+
+using System;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using PluginCore.IPlugins;
+
+namespace PluginCore.AspNetCore.Middlewares
+{
+ public class LanguageMiddleware
+ {
+ private readonly RequestDelegate _next;
+
+ public LanguageMiddleware(RequestDelegate next)
+ {
+ _next = next;
+ }
+
+ public async Task Invoke(HttpContext httpContext)
+ {
+ // 从 Cookie 中获取语言标识
+ string language = httpContext.Request.Cookies[Constants.AspNetCoreLanguageCookieName];
+
+ // 存储当前语言到 HttpContext.Items
+ httpContext.Items[Constants.AspNetCoreLanguageKey] = language;
+
+ // 调用下一个中间件
+ await _next(httpContext);
+ }
+ }
+
+ public static class LanguageMiddlewareExtensions
+ {
+ public static IApplicationBuilder UsePluginCoreLanguageMiddleware(this IApplicationBuilder builder)
+ {
+ return builder.UseMiddleware();
+ }
+ }
+}
diff --git a/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Middlewares/PluginContentFilterMiddleware.cs b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Middlewares/PluginContentFilterMiddleware.cs
new file mode 100644
index 00000000..7ade9b06
--- /dev/null
+++ b/Admin.NET/Plugins/PluginCore/PluginCore.AspNetCore/Middlewares/PluginContentFilterMiddleware.cs
@@ -0,0 +1,150 @@
+//===================================================
+// License: GNU LGPLv3
+// Contributors: yiyungent@gmail.com
+// Project: https://yiyungent.github.io/PluginCore
+// GitHub: https://github.com/yiyungent/PluginCore
+//===================================================
+
+
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+using PluginCore.Interfaces;
+
+namespace PluginCore.AspNetCore.Middlewares
+{
+ ///