diff --git a/aspnet-core/.editorconfig b/aspnet-core/.editorconfig new file mode 100644 index 000000000..2289110b2 --- /dev/null +++ b/aspnet-core/.editorconfig @@ -0,0 +1,137 @@ +# Rules in this file were initially inferred by Visual Studio IntelliCode from the D:\Projects\MicroService\CRM\Vue\abp-next-admin\aspnet-core codebase based on best match to current usage at 2021-12-27 +# You can modify the rules from these initially generated values to suit your own policies +# You can learn more about editorconfig here: https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference +[*.cs] + + +#Core editorconfig formatting - indentation + +#use soft tabs (spaces) for indentation +indent_style = space + +#Formatting - indentation options + +#indent switch case contents. +csharp_indent_case_contents = true +#indent switch labels +csharp_indent_switch_labels = true + +#Formatting - new line options + +#place catch statements on a new line +csharp_new_line_before_catch = true +#place else statements on a new line +csharp_new_line_before_else = true +#require members of anonymous types to be on separate lines +csharp_new_line_before_members_in_anonymous_types = true +#require members of object initializers to be on the same line +csharp_new_line_before_members_in_object_initializers = false +#require braces to be on a new line for anonymous_types, control_blocks, types, lambdas, object_collection_array_initializers, methods, and anonymous_methods (also known as "Allman" style) +csharp_new_line_before_open_brace = anonymous_types, control_blocks, types, lambdas, object_collection_array_initializers, methods, anonymous_methods + +#Formatting - organize using options + +#do not place System.* using directives before other using directives +dotnet_sort_system_directives_first = false + +#Formatting - spacing options + +#require NO space between a cast and the value +csharp_space_after_cast = false +#require a space before the colon for bases or interfaces in a type declaration +csharp_space_after_colon_in_inheritance_clause = true +#require a space after a keyword in a control flow statement such as a for loop +csharp_space_after_keywords_in_control_flow_statements = true +#require a space before the colon for bases or interfaces in a type declaration +csharp_space_before_colon_in_inheritance_clause = true +#remove space within empty argument list parentheses +csharp_space_between_method_call_empty_parameter_list_parentheses = false +#remove space between method call name and opening parenthesis +csharp_space_between_method_call_name_and_opening_parenthesis = false +#do not place space characters after the opening parenthesis and before the closing parenthesis of a method call +csharp_space_between_method_call_parameter_list_parentheses = false +#remove space within empty parameter list parentheses for a method declaration +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +#place a space character after the opening parenthesis and before the closing parenthesis of a method declaration parameter list. +csharp_space_between_method_declaration_parameter_list_parentheses = false + +#Formatting - wrapping options + +#leave code block on single line +csharp_preserve_single_line_blocks = true +#leave statements and member declarations on the same line +csharp_preserve_single_line_statements = true + +#Style - Code block preferences + +#prefer curly braces even for one line of code +csharp_prefer_braces = true:suggestion + +#Style - expression bodied member options + +#prefer block bodies for constructors +csharp_style_expression_bodied_constructors = false:suggestion +#prefer block bodies for methods +csharp_style_expression_bodied_methods = false:suggestion +#prefer expression-bodied members for properties +csharp_style_expression_bodied_properties = true:suggestion + +#Style - expression level options + +#prefer out variables to be declared inline in the argument list of a method call when possible +csharp_style_inlined_variable_declaration = true:suggestion +#prefer the language keyword for member access expressions, instead of the type name, for types that have a keyword to represent them +dotnet_style_predefined_type_for_member_access = true:suggestion + +#Style - Expression-level preferences + +#prefer default over default(T) +csharp_prefer_simple_default_expression = true:suggestion +#prefer objects to be initialized using object initializers when possible +dotnet_style_object_initializer = true:suggestion +#prefer inferred anonymous type member names +dotnet_style_prefer_inferred_anonymous_type_member_names = false:suggestion + +#Style - implicit and explicit types + +#prefer var over explicit type in all cases, unless overridden by another code style rule +csharp_style_var_elsewhere = true:suggestion +#prefer var is used to declare variables with built-in system types such as int +csharp_style_var_for_built_in_types = true:suggestion +#prefer var when the type is already mentioned on the right-hand side of a declaration expression +csharp_style_var_when_type_is_apparent = true:suggestion + +#Style - language keyword and framework type options + +#prefer the language keyword for local variables, method parameters, and class members, instead of the type name, for types that have a keyword to represent them +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion + +#Style - Miscellaneous preferences + +#prefer anonymous functions over local functions +csharp_style_pattern_local_over_anonymous_function = false:suggestion + +#Style - modifier options + +#prefer accessibility modifiers to be declared except for public interface members. This will currently not differ from always and will act as future proofing for if C# adds default interface methods. +dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion + +#Style - Modifier preferences + +#when this rule is set to a list of modifiers, prefer the specified ordering. +csharp_preferred_modifier_order = public,private,protected,internal,async,virtual,readonly,static,override,abstract:suggestion + +#Style - Pattern matching + +#prefer pattern matching instead of is expression with type casts +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion + +#Style - qualification options + +#prefer fields not to be prefaced with this. or Me. in Visual Basic +dotnet_style_qualification_for_field = false:suggestion +#prefer methods not to be prefaced with this. or Me. in Visual Basic +dotnet_style_qualification_for_method = false:suggestion +#prefer properties not to be prefaced with this. or Me. in Visual Basic +dotnet_style_qualification_for_property = false:suggestion +csharp_style_namespace_declarations=file_scoped:silent diff --git a/aspnet-core/Directory.Build.props b/aspnet-core/Directory.Build.props index a84b845ce..fead421c4 100644 --- a/aspnet-core/Directory.Build.props +++ b/aspnet-core/Directory.Build.props @@ -7,6 +7,7 @@ 5.2.0 1.5.10 2.13.0 + 3.0.434 1.2.1.5 2.0.3 1.7.27 diff --git a/aspnet-core/LINGYUN.MicroService.All.sln b/aspnet-core/LINGYUN.MicroService.All.sln index bc28da9f3..6448be090 100644 --- a/aspnet-core/LINGYUN.MicroService.All.sln +++ b/aspnet-core/LINGYUN.MicroService.All.sln @@ -363,9 +363,22 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.DataProtection" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.DataProtection.EntityFrameworkCore", "modules\data-protection\LINGYUN.Abp.DataProtection.EntityFrameworkCore\LINGYUN.Abp.DataProtection.EntityFrameworkCore.csproj", "{19B860CA-2E1E-45CC-A5E2-ED3F2BCEAB5D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LINGYUN.Abp.IdGenerator", "modules\common\LINGYUN.Abp.IdGenerator\LINGYUN.Abp.IdGenerator.csproj", "{440C9BD9-85EA-4473-AB1C-7C3562DF4915}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.IdGenerator", "modules\common\LINGYUN.Abp.IdGenerator\LINGYUN.Abp.IdGenerator.csproj", "{440C9BD9-85EA-4473-AB1C-7C3562DF4915}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LINGYUN.Abp.Serilog.Enrichers.UniqueId", "modules\logging\LINGYUN.Abp.Serilog.Enrichers.UniqueId\LINGYUN.Abp.Serilog.Enrichers.UniqueId.csproj", "{23C3B247-523A-4FBF-B785-2F035E0089BD}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Serilog.Enrichers.UniqueId", "modules\logging\LINGYUN.Abp.Serilog.Enrichers.UniqueId\LINGYUN.Abp.Serilog.Enrichers.UniqueId.csproj", "{23C3B247-523A-4FBF-B785-2F035E0089BD}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{A76A4741-AF91-44EF-A7B6-8E7AF4961146}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Tencent.SettingManagement", "modules\cloud-tencent\LINGYUN.Abp.Tencent.SettingManagement\LINGYUN.Abp.Tencent.SettingManagement.csproj", "{C7CF4193-6397-4450-AF42-3BACD7CF292E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Sms.Tencent", "modules\cloud-tencent\LINGYUN.Abp.Sms.Tencent\LINGYUN.Abp.Sms.Tencent.csproj", "{8FE2725C-6829-4778-93BA-A53260697AFB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.BlobStoring.Tencent", "modules\cloud-tencent\LINGYUN.Abp.BlobStoring.Tencent\LINGYUN.Abp.BlobStoring.Tencent.csproj", "{A4B972EC-9F0B-4405-9965-766FABC9B07E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LINGYUN.Abp.OssManagement.Tencent", "modules\oss-management\LINGYUN.Abp.OssManagement.Tencent\LINGYUN.Abp.OssManagement.Tencent.csproj", "{31E60E23-FD98-4D5E-A137-2B3F2968BA09}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -953,6 +966,22 @@ Global {23C3B247-523A-4FBF-B785-2F035E0089BD}.Debug|Any CPU.Build.0 = Debug|Any CPU {23C3B247-523A-4FBF-B785-2F035E0089BD}.Release|Any CPU.ActiveCfg = Release|Any CPU {23C3B247-523A-4FBF-B785-2F035E0089BD}.Release|Any CPU.Build.0 = Release|Any CPU + {C7CF4193-6397-4450-AF42-3BACD7CF292E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C7CF4193-6397-4450-AF42-3BACD7CF292E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C7CF4193-6397-4450-AF42-3BACD7CF292E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C7CF4193-6397-4450-AF42-3BACD7CF292E}.Release|Any CPU.Build.0 = Release|Any CPU + {8FE2725C-6829-4778-93BA-A53260697AFB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8FE2725C-6829-4778-93BA-A53260697AFB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8FE2725C-6829-4778-93BA-A53260697AFB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8FE2725C-6829-4778-93BA-A53260697AFB}.Release|Any CPU.Build.0 = Release|Any CPU + {A4B972EC-9F0B-4405-9965-766FABC9B07E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A4B972EC-9F0B-4405-9965-766FABC9B07E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A4B972EC-9F0B-4405-9965-766FABC9B07E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A4B972EC-9F0B-4405-9965-766FABC9B07E}.Release|Any CPU.Build.0 = Release|Any CPU + {31E60E23-FD98-4D5E-A137-2B3F2968BA09}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {31E60E23-FD98-4D5E-A137-2B3F2968BA09}.Debug|Any CPU.Build.0 = Debug|Any CPU + {31E60E23-FD98-4D5E-A137-2B3F2968BA09}.Release|Any CPU.ActiveCfg = Release|Any CPU + {31E60E23-FD98-4D5E-A137-2B3F2968BA09}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1134,6 +1163,10 @@ Global {19B860CA-2E1E-45CC-A5E2-ED3F2BCEAB5D} = {529DF802-97C4-4BF2-BE7C-39663B3D9EA3} {440C9BD9-85EA-4473-AB1C-7C3562DF4915} = {8AC72641-30D3-4ACF-89FA-808FADC55C2E} {23C3B247-523A-4FBF-B785-2F035E0089BD} = {6FC0578B-CDF1-43AD-9F7E-4AA7E4720A02} + {C7CF4193-6397-4450-AF42-3BACD7CF292E} = {3B96F4D8-4993-419B-BCEB-AFE4ED39449F} + {8FE2725C-6829-4778-93BA-A53260697AFB} = {3B96F4D8-4993-419B-BCEB-AFE4ED39449F} + {A4B972EC-9F0B-4405-9965-766FABC9B07E} = {3B96F4D8-4993-419B-BCEB-AFE4ED39449F} + {31E60E23-FD98-4D5E-A137-2B3F2968BA09} = {B05CB08F-C088-4D6D-97EE-A94A5D1AE4A6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {C95FDF91-16F2-4A8B-A4BE-0E62D1B66718} diff --git a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.BlobStoring.Tencent/LINGYUN.Abp.BlobStoring.Tencent.csproj b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.BlobStoring.Tencent/LINGYUN.Abp.BlobStoring.Tencent.csproj new file mode 100644 index 000000000..9697fa205 --- /dev/null +++ b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.BlobStoring.Tencent/LINGYUN.Abp.BlobStoring.Tencent.csproj @@ -0,0 +1,21 @@ + + + + + + + netstandard2.0 + + 腾讯云Oss对象存储Abp集成 + + + + + + + + + + + + diff --git a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.BlobStoring.Tencent/LINGYUN/Abp/BlobStoring/Tencent/AbpBlobStoringTencentCloudModule.cs b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.BlobStoring.Tencent/LINGYUN/Abp/BlobStoring/Tencent/AbpBlobStoringTencentCloudModule.cs new file mode 100644 index 000000000..db9a6c0dd --- /dev/null +++ b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.BlobStoring.Tencent/LINGYUN/Abp/BlobStoring/Tencent/AbpBlobStoringTencentCloudModule.cs @@ -0,0 +1,12 @@ +using LINGYUN.Abp.Tencent; +using Volo.Abp.BlobStoring; +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.BlobStoring.Tencent; + +[DependsOn( + typeof(AbpBlobStoringModule), + typeof(AbpTencentCloudModule))] +public class AbpBlobStoringTencentCloudModule : AbpModule +{ +} \ No newline at end of file diff --git a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.BlobStoring.Tencent/LINGYUN/Abp/BlobStoring/Tencent/CosClientFactory.cs b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.BlobStoring.Tencent/LINGYUN/Abp/BlobStoring/Tencent/CosClientFactory.cs new file mode 100644 index 000000000..d45c1846e --- /dev/null +++ b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.BlobStoring.Tencent/LINGYUN/Abp/BlobStoring/Tencent/CosClientFactory.cs @@ -0,0 +1,51 @@ +using COSXML; +using COSXML.Auth; +using LINGYUN.Abp.Tencent; +using Microsoft.Extensions.Caching.Memory; +using System.Threading.Tasks; +using Volo.Abp.BlobStoring; +using Volo.Abp.DependencyInjection; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Settings; + +namespace LINGYUN.Abp.BlobStoring.Tencent; + +public class CosClientFactory : AbstractTencentCloudClientFactory, + ICosClientFactory, + ITransientDependency +{ + protected IBlobContainerConfigurationProvider ConfigurationProvider { get; } + + public CosClientFactory( + IMemoryCache clientCache, + ICurrentTenant currentTenant, + ISettingProvider settingProvider, + IBlobContainerConfigurationProvider configurationProvider) + : base(clientCache, currentTenant, settingProvider) + { + ConfigurationProvider = configurationProvider; + } + + public virtual async Task CreateAsync() + { + var configuration = ConfigurationProvider.Get(); + + return await CreateAsync(configuration.GetTencentConfiguration()); + } + + protected override CosXml CreateClient(TencentBlobProviderConfiguration configuration, TencentCloudClientCacheItem cloudCache) + { + var configBuilder = new CosXmlConfig.Builder(); + configBuilder + .SetAppid(configuration.AppId) + .SetRegion(configuration.Region); + + var cred = new DefaultQCloudCredentialProvider( + cloudCache.SecretId, + cloudCache.SecretKey, + cloudCache.DurationSecond); + + // TODO: 推荐全局单个对象,需要解决缓存过期事件 + return new CosXmlServer(configBuilder.Build(), cred); + } +} diff --git a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.BlobStoring.Tencent/LINGYUN/Abp/BlobStoring/Tencent/DefaultTencentBlobNameCalculator.cs b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.BlobStoring.Tencent/LINGYUN/Abp/BlobStoring/Tencent/DefaultTencentBlobNameCalculator.cs new file mode 100644 index 000000000..a4ae97625 --- /dev/null +++ b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.BlobStoring.Tencent/LINGYUN/Abp/BlobStoring/Tencent/DefaultTencentBlobNameCalculator.cs @@ -0,0 +1,24 @@ +using Volo.Abp.BlobStoring; +using Volo.Abp.DependencyInjection; +using Volo.Abp.MultiTenancy; + +namespace LINGYUN.Abp.BlobStoring.Tencent +{ + public class DefaultTencentBlobNameCalculator : ITencentBlobNameCalculator, ITransientDependency + { + protected ICurrentTenant CurrentTenant { get; } + + public DefaultTencentBlobNameCalculator( + ICurrentTenant currentTenant) + { + CurrentTenant = currentTenant; + } + + public string Calculate(BlobProviderArgs args) + { + return CurrentTenant.Id == null + ? $"host/{args.BlobName}" + : $"tenants/{CurrentTenant.Id.Value:D}/{args.BlobName}"; + } + } +} diff --git a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.BlobStoring.Tencent/LINGYUN/Abp/BlobStoring/Tencent/ICosClientFactory.cs b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.BlobStoring.Tencent/LINGYUN/Abp/BlobStoring/Tencent/ICosClientFactory.cs new file mode 100644 index 000000000..29127d49f --- /dev/null +++ b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.BlobStoring.Tencent/LINGYUN/Abp/BlobStoring/Tencent/ICosClientFactory.cs @@ -0,0 +1,11 @@ +using COSXML; +using System.Threading.Tasks; + +namespace LINGYUN.Abp.BlobStoring.Tencent; + +public interface ICosClientFactory +{ + Task CreateAsync(); + + Task CreateAsync(TencentBlobProviderConfiguration configuration); +} diff --git a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.BlobStoring.Tencent/LINGYUN/Abp/BlobStoring/Tencent/ITencentBlobNameCalculator.cs b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.BlobStoring.Tencent/LINGYUN/Abp/BlobStoring/Tencent/ITencentBlobNameCalculator.cs new file mode 100644 index 000000000..4ab662c4f --- /dev/null +++ b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.BlobStoring.Tencent/LINGYUN/Abp/BlobStoring/Tencent/ITencentBlobNameCalculator.cs @@ -0,0 +1,9 @@ +using Volo.Abp.BlobStoring; + +namespace LINGYUN.Abp.BlobStoring.Tencent +{ + public interface ITencentBlobNameCalculator + { + string Calculate(BlobProviderArgs args); + } +} diff --git a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.BlobStoring.Tencent/LINGYUN/Abp/BlobStoring/Tencent/TencentBlobContainerConfigurationExtensions.cs b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.BlobStoring.Tencent/LINGYUN/Abp/BlobStoring/Tencent/TencentBlobContainerConfigurationExtensions.cs new file mode 100644 index 000000000..f86663dfb --- /dev/null +++ b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.BlobStoring.Tencent/LINGYUN/Abp/BlobStoring/Tencent/TencentBlobContainerConfigurationExtensions.cs @@ -0,0 +1,25 @@ +using System; +using Volo.Abp.BlobStoring; + +namespace LINGYUN.Abp.BlobStoring.Tencent +{ + public static class TencentBlobContainerConfigurationExtensions + { + public static TencentBlobProviderConfiguration GetTencentConfiguration( + this BlobContainerConfiguration containerConfiguration) + { + return new TencentBlobProviderConfiguration(containerConfiguration); + } + + public static BlobContainerConfiguration UseTencentCloud( + this BlobContainerConfiguration containerConfiguration, + Action aliyunConfigureAction) + { + containerConfiguration.ProviderType = typeof(TencentCloudBlobProvider); + + aliyunConfigureAction(new TencentBlobProviderConfiguration(containerConfiguration)); + + return containerConfiguration; + } + } +} diff --git a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.BlobStoring.Tencent/LINGYUN/Abp/BlobStoring/Tencent/TencentBlobNamingNormalizer.cs b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.BlobStoring.Tencent/LINGYUN/Abp/BlobStoring/Tencent/TencentBlobNamingNormalizer.cs new file mode 100644 index 000000000..1db121278 --- /dev/null +++ b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.BlobStoring.Tencent/LINGYUN/Abp/BlobStoring/Tencent/TencentBlobNamingNormalizer.cs @@ -0,0 +1,61 @@ +using System.Text.RegularExpressions; +using Volo.Abp.BlobStoring; +using Volo.Abp.DependencyInjection; + +namespace LINGYUN.Abp.BlobStoring.Tencent +{ + public class TencentBlobNamingNormalizer : IBlobNamingNormalizer, ITransientDependency + { + /// + /// 腾讯云对象命名规范 + /// https://cloud.tencent.com/document/product/436/13324 + /// + /// + /// + public virtual string NormalizeBlobName(string blobName) + { + // 不允许以正斜线/或者反斜线\开头。 + blobName = Regex.Replace(blobName, "^/", string.Empty); + blobName = blobName.StartsWith("\\") ? blobName.Substring(1) : blobName; + + // 对象键中不支持 ASCII 控制字符中的 + // 字符上(↑),字符下(↓),字符右(→),字符左(←), + // 分别对应 CAN(24),EM(25),SUB(26),ESC(27)。 + blobName = blobName.Replace("↑", ""); + blobName = blobName.Replace("↓", ""); + blobName = blobName.Replace("←", ""); + blobName = blobName.Replace("→", ""); + + // TODO: 要求还真多...其他暂时不写了 + + return blobName; + } + + /// + /// 腾讯云BucketName命名规范 + /// https://cloud.tencent.com/document/product/436/13312 + /// + /// + /// + public virtual string NormalizeContainerName(string containerName) + { + // 仅支持小写英文字母和数字,即[a-z,0-9]、中划线“-”及其组合。 + containerName = containerName.ToLower(); + containerName = Regex.Replace(containerName, "[^a-z0-9-]", string.Empty); + + // 不能以短划线(-)开头 + containerName = Regex.Replace(containerName, "^-", string.Empty); + // 不能以短划线(-)结尾 + containerName = Regex.Replace(containerName, "-$", string.Empty); + + // 存储桶名称的最大允许字符受到 地域简称 和 APPID 的字符数影响,组成的完整请求域名字符数总计最多60个字符。 + // 例如请求域名123456789012345678901-1250000000.cos.ap-beijing.myqcloud.com总和为60个字符。 + if (containerName.Length > 60) + { + containerName = containerName.Substring(0, 60); + } + + return containerName; + } + } +} diff --git a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.BlobStoring.Tencent/LINGYUN/Abp/BlobStoring/Tencent/TencentBlobProviderConfiguration.cs b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.BlobStoring.Tencent/LINGYUN/Abp/BlobStoring/Tencent/TencentBlobProviderConfiguration.cs new file mode 100644 index 000000000..270e3fb9b --- /dev/null +++ b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.BlobStoring.Tencent/LINGYUN/Abp/BlobStoring/Tencent/TencentBlobProviderConfiguration.cs @@ -0,0 +1,63 @@ +using System.Collections.Generic; +using Volo.Abp; +using Volo.Abp.BlobStoring; + +namespace LINGYUN.Abp.BlobStoring.Tencent +{ + public class TencentBlobProviderConfiguration + { + /// + /// AppId + /// + public string AppId { + get => _containerConfiguration.GetConfiguration(TencentBlobProviderConfigurationNames.AppId); + set => _containerConfiguration.SetConfiguration(TencentBlobProviderConfigurationNames.AppId, Check.NotNullOrWhiteSpace(value, nameof(value))); + } + /// + /// 区域 + /// + public string Region { + get => _containerConfiguration.GetConfiguration(TencentBlobProviderConfigurationNames.Region); + set => _containerConfiguration.SetConfiguration(TencentBlobProviderConfigurationNames.Region, value); + } + /// + /// 命名空间 + /// + public string BucketName + { + get => _containerConfiguration.GetConfiguration(TencentBlobProviderConfigurationNames.BucketName); + set => _containerConfiguration.SetConfiguration(TencentBlobProviderConfigurationNames.BucketName, Check.NotNullOrWhiteSpace(value, nameof(value))); + } + /// + /// 命名空间不存在是否创建 + /// + public bool CreateBucketIfNotExists + { + get => _containerConfiguration.GetConfigurationOrDefault(TencentBlobProviderConfigurationNames.CreateBucketIfNotExists, false); + set => _containerConfiguration.SetConfiguration(TencentBlobProviderConfigurationNames.CreateBucketIfNotExists, value); + } + /// + /// 创建命名空间时防盗链列表 + /// + public List CreateBucketReferer { + get => _containerConfiguration.GetConfiguration>(TencentBlobProviderConfigurationNames.CreateBucketReferer); + set { + if (value == null) + { + _containerConfiguration.SetConfiguration(TencentBlobProviderConfigurationNames.CreateBucketReferer, new List()); + } + else + { + _containerConfiguration.SetConfiguration(TencentBlobProviderConfigurationNames.CreateBucketReferer, value); + } + } + } + + private readonly BlobContainerConfiguration _containerConfiguration; + + public TencentBlobProviderConfiguration(BlobContainerConfiguration containerConfiguration) + { + _containerConfiguration = containerConfiguration; + } + } +} diff --git a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.BlobStoring.Tencent/LINGYUN/Abp/BlobStoring/Tencent/TencentBlobProviderConfigurationNames.cs b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.BlobStoring.Tencent/LINGYUN/Abp/BlobStoring/Tencent/TencentBlobProviderConfigurationNames.cs new file mode 100644 index 000000000..dbe79e467 --- /dev/null +++ b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.BlobStoring.Tencent/LINGYUN/Abp/BlobStoring/Tencent/TencentBlobProviderConfigurationNames.cs @@ -0,0 +1,26 @@ +namespace LINGYUN.Abp.BlobStoring.Tencent +{ + public static class TencentBlobProviderConfigurationNames + { + /// + /// AppId + /// + public const string AppId = "Tencent:OSS:AppId"; + /// + /// 区域 + /// + public const string Region = "Tencent:OSS:Region"; + /// + /// 命名空间 + /// + public const string BucketName = "Tencent:OSS:BucketName"; + /// + /// 命名空间不存在是否创建 + /// + public const string CreateBucketIfNotExists = "Tencent:OSS:CreateBucketIfNotExists"; + /// + /// 创建命名空间时防盗链列表 + /// + public const string CreateBucketReferer = "Tencent:OSS:CreateBucketReferer"; + } +} diff --git a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.BlobStoring.Tencent/LINGYUN/Abp/BlobStoring/Tencent/TencentCloudBlobProvider.cs b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.BlobStoring.Tencent/LINGYUN/Abp/BlobStoring/Tencent/TencentCloudBlobProvider.cs new file mode 100644 index 000000000..e6e0adf1e --- /dev/null +++ b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.BlobStoring.Tencent/LINGYUN/Abp/BlobStoring/Tencent/TencentCloudBlobProvider.cs @@ -0,0 +1,165 @@ +using COSXML; +using COSXML.Common; +using COSXML.Model.Bucket; +using COSXML.Model.Object; +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Volo.Abp.BlobStoring; +using Volo.Abp.DependencyInjection; + +namespace LINGYUN.Abp.BlobStoring.Tencent; + +public class TencentCloudBlobProvider : BlobProviderBase, ITransientDependency +{ + protected ICosClientFactory CosClientFactory { get; } + protected ITencentBlobNameCalculator TencentBlobNameCalculator { get; } + + public TencentCloudBlobProvider( + ICosClientFactory cosClientFactory, + ITencentBlobNameCalculator tencentBlobNameCalculator) + { + CosClientFactory = cosClientFactory; + TencentBlobNameCalculator = tencentBlobNameCalculator; + } + + public override async Task DeleteAsync(BlobProviderDeleteArgs args) + { + var ossClient = await GetOssClientAsync(args); + var blobName = TencentBlobNameCalculator.Calculate(args); + + if (await BlobExistsAsync(ossClient, args, blobName)) + { + var request = new DeleteObjectRequest(GetBucketName(args), blobName); + return ossClient.DeleteObject(request).IsSuccessful(); + } + + return false; + } + + public override async Task ExistsAsync(BlobProviderExistsArgs args) + { + var ossClient = await GetOssClientAsync(args); + var blobName = TencentBlobNameCalculator.Calculate(args); + + return await BlobExistsAsync(ossClient, args, blobName); + } + + public override async Task GetOrNullAsync(BlobProviderGetArgs args) + { + var ossClient = await GetOssClientAsync(args); + var blobName = TencentBlobNameCalculator.Calculate(args); + + if (!await BlobExistsAsync(ossClient, args, blobName)) + { + return null; + } + + // TODO: 未经验证 + + var request = new GetObjectBytesRequest(GetBucketName(args), blobName); + var ossObject = ossClient.GetObject(request); + var memoryStream = new MemoryStream(); + await memoryStream.WriteAsync(ossObject.content, 0, ossObject.content.Length); + memoryStream.Seek(0, SeekOrigin.Begin); + return memoryStream; + } + + public override async Task SaveAsync(BlobProviderSaveArgs args) + { + var ossClient = await GetOssClientAsync(args); + var blobName = TencentBlobNameCalculator.Calculate(args); + var configuration = args.Configuration.GetTencentConfiguration(); + + // 先检查Bucket + if (configuration.CreateBucketIfNotExists) + { + await CreateBucketIfNotExists(ossClient, args, configuration.CreateBucketReferer); + } + + var bucketName = GetBucketName(args); + + // 是否已存在 + if (await BlobExistsAsync(ossClient, args, blobName)) + { + // 是否覆盖 + if (!args.OverrideExisting) + { + throw new BlobAlreadyExistsException($"Saving BLOB '{args.BlobName}' does already exists in the bucketName '{GetBucketName(args)}'! Set {nameof(args.OverrideExisting)} if it should be overwritten."); + } + else + { + // 删除原文件 + var deleteRequest = new DeleteObjectRequest(bucketName, blobName); + ossClient.DeleteObject(deleteRequest); + } + } + // 保存 + var putRequest = new PutObjectRequest(bucketName, blobName, args.BlobStream); + ossClient.PutObject(putRequest); + } + + protected virtual async Task GetOssClientAsync(BlobProviderArgs args) + { + var configuration = args.Configuration.GetTencentConfiguration(); + var ossClient = await CosClientFactory.CreateAsync(configuration); + return ossClient; + } + + protected virtual async Task CreateBucketIfNotExists(CosXml cos, BlobProviderArgs args, IList refererList = null) + { + if (!await BucketExistsAsync(cos, args)) + { + var bucketName = GetBucketName(args); + + var request = new PutBucketRequest(bucketName); + // TODO: good! 这很Java + request.SetCosACL(CosACL.PublicReadWrite); + + cos.PutBucket(request); + + if (refererList != null && refererList.Count > 0) + { + var srq = new PutBucketRefererRequest(bucketName); + var refererConfig = new COSXML.Model.Tag.RefererConfiguration(); + foreach (var domain in refererList) + { + refererConfig.domainList.AddDomain(domain); + } + srq.SetRefererConfiguration(refererConfig); + + cos.PutBucketReferer(srq); + } + } + } + + private async Task BlobExistsAsync(CosXml cos, BlobProviderArgs args, string blobName) + { + var bucketExists = await BucketExistsAsync(cos, args); + if (bucketExists) + { + var request = new DoesObjectExistRequest(GetBucketName(args), blobName); + var objectExists = cos.DoesObjectExist(request); + + return objectExists; + } + return false; + } + + private Task BucketExistsAsync(CosXml cos, BlobProviderArgs args) + { + var request = new DoesBucketExistRequest(GetBucketName(args)); + var bucketExists = cos.DoesBucketExist(request); + + return Task.FromResult(bucketExists); + } + + private static string GetBucketName(BlobProviderArgs args) + { + var configuration = args.Configuration.GetTencentConfiguration(); + return configuration.BucketName.IsNullOrWhiteSpace() + ? args.ContainerName + : configuration.BucketName; + } +} diff --git a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Sms.Tencent/LINGYUN.Abp.Sms.Tencent.csproj b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Sms.Tencent/LINGYUN.Abp.Sms.Tencent.csproj new file mode 100644 index 000000000..75746b80c --- /dev/null +++ b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Sms.Tencent/LINGYUN.Abp.Sms.Tencent.csproj @@ -0,0 +1,23 @@ + + + + netstandard2.0 + + + + + + + + + + + + + + + + + + + diff --git a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Sms.Tencent/LINGYUN/Abp/Sms/Tencent/AbpSmsTencentModule.cs b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Sms.Tencent/LINGYUN/Abp/Sms/Tencent/AbpSmsTencentModule.cs new file mode 100644 index 000000000..f808861aa --- /dev/null +++ b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Sms.Tencent/LINGYUN/Abp/Sms/Tencent/AbpSmsTencentModule.cs @@ -0,0 +1,30 @@ +using LINGYUN.Abp.Tencent; +using LINGYUN.Abp.Tencent.Localization; +using Volo.Abp.Localization; +using Volo.Abp.Modularity; +using Volo.Abp.Sms; +using Volo.Abp.VirtualFileSystem; + +namespace LINGYUN.Abp.Sms.Tencent +{ + [DependsOn( + typeof(AbpSmsModule), + typeof(AbpTencentCloudModule))] + public class AbpSmsTencentModule : AbpModule + { + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.FileSets.AddEmbedded(); + }); + + Configure(options => + { + options.Resources + .Get() + .AddVirtualJson("/LINGYUN/Abp/Sms/Tencent/Localization/Resources"); + }); + } + } +} diff --git a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Sms.Tencent/LINGYUN/Abp/Sms/Tencent/Localization/Resources/en.json b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Sms.Tencent/LINGYUN/Abp/Sms/Tencent/Localization/Resources/en.json new file mode 100644 index 000000000..5cbf681ec --- /dev/null +++ b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Sms.Tencent/LINGYUN/Abp/Sms/Tencent/Localization/Resources/en.json @@ -0,0 +1,12 @@ +{ + "culture": "en", + "texts": { + "DisplayName:TenantCloud.SmsSetting": "Sms Setting", + "DisplayName:AppId": "App Id", + "Description:AppId": "AppId applied on Tencent Cloud SMS control platform.", + "DisplayName:DefaultSignName": "Default Sign Name", + "Description:DefaultSignName": "Default signname if no signname is specified in the SMS message body", + "DisplayName:DefaultTemplateId": "Default Template Id", + "Description:DefaultTemplateId": "Default template id if the Template Id number is not specified in the SMS message body." + } +} \ No newline at end of file diff --git a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Sms.Tencent/LINGYUN/Abp/Sms/Tencent/Localization/Resources/zh-Hans.json b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Sms.Tencent/LINGYUN/Abp/Sms/Tencent/Localization/Resources/zh-Hans.json new file mode 100644 index 000000000..2f6f529bf --- /dev/null +++ b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Sms.Tencent/LINGYUN/Abp/Sms/Tencent/Localization/Resources/zh-Hans.json @@ -0,0 +1,12 @@ +{ + "culture": "zh-Hans", + "texts": { + "DisplayName:TenantCloud.SmsSetting": "短信设置", + "DisplayName:AppId": "应用Id", + "Description:AppId": "在腾讯云短信控制平台申请的应用标识", + "DisplayName:DefaultSignName": "默认签名", + "Description:DefaultSignName": "当短信消息体未指定签名时的默认签名", + "DisplayName:DefaultTemplateId": "默认模板", + "Description:DefaultTemplateId": "当短信消息体未指定模板号时的默认模板标识" + } +} \ No newline at end of file diff --git a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Sms.Tencent/LINGYUN/Abp/Sms/Tencent/Settings/TencentCloudSmsSettingNames.cs b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Sms.Tencent/LINGYUN/Abp/Sms/Tencent/Settings/TencentCloudSmsSettingNames.cs new file mode 100644 index 000000000..185f31eb0 --- /dev/null +++ b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Sms.Tencent/LINGYUN/Abp/Sms/Tencent/Settings/TencentCloudSmsSettingNames.cs @@ -0,0 +1,23 @@ +using LINGYUN.Abp.Tencent.Settings; + +namespace LINGYUN.Abp.Sms.Tencent.Settings +{ + public static class TencentCloudSmsSettingNames + { + public const string Prefix = TencentCloudSettingNames.Prefix + ".Sms"; + + /// + /// 短信 SdkAppId + /// 在 短信控制台 添加应用后生成的实际 SdkAppId,示例如1400006666。 + /// + public const string AppId = Prefix + ".Domain"; + /// + /// 短信签名内容 + /// + public const string DefaultSignName = Prefix + ".DefaultSignName"; + /// + /// 默认短信模板 ID + /// + public const string DefaultTemplateId = Prefix + ".DefaultTemplateId"; + } +} diff --git a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Sms.Tencent/LINGYUN/Abp/Sms/Tencent/Settings/TencentCloudSmsSettingProvider.cs b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Sms.Tencent/LINGYUN/Abp/Sms/Tencent/Settings/TencentCloudSmsSettingProvider.cs new file mode 100644 index 000000000..0c4a4e72f --- /dev/null +++ b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Sms.Tencent/LINGYUN/Abp/Sms/Tencent/Settings/TencentCloudSmsSettingProvider.cs @@ -0,0 +1,59 @@ +using LINGYUN.Abp.Tencent.Localization; +using Volo.Abp.Localization; +using Volo.Abp.Settings; + +namespace LINGYUN.Abp.Sms.Tencent.Settings +{ + public class TencentCloudSmsSettingProvider : SettingDefinitionProvider + { + public override void Define(ISettingDefinitionContext context) + { + context.Add(CreateAliyunSettings()); + } + + private SettingDefinition[] CreateAliyunSettings() + { + return new SettingDefinition[] + { + new SettingDefinition( + TencentCloudSmsSettingNames.AppId, + displayName: L("DisplayName:AppId"), + description: L("Description:AppId"), + isVisibleToClients: false, + isEncrypted: true + ) + .WithProviders( + DefaultValueSettingValueProvider.ProviderName, + GlobalSettingValueProvider.ProviderName, + TenantSettingValueProvider.ProviderName), + new SettingDefinition( + TencentCloudSmsSettingNames.DefaultSignName, + displayName: L("DisplayName:DefaultSignName"), + description: L("Description:DefaultSignName"), + isVisibleToClients: false, + isEncrypted: true + ) + .WithProviders( + DefaultValueSettingValueProvider.ProviderName, + GlobalSettingValueProvider.ProviderName, + TenantSettingValueProvider.ProviderName), + new SettingDefinition( + TencentCloudSmsSettingNames.DefaultTemplateId, + displayName: L("DisplayName:DefaultTemplateId"), + description: L("Description:DefaultTemplateId"), + isVisibleToClients: false, + isEncrypted: true + ) + .WithProviders( + DefaultValueSettingValueProvider.ProviderName, + GlobalSettingValueProvider.ProviderName, + TenantSettingValueProvider.ProviderName), + }; + } + + private ILocalizableString L(string name) + { + return LocalizableString.Create(name); + } + } +} diff --git a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Sms.Tencent/LINGYUN/Abp/Sms/Tencent/TencentCloudSmsSender.cs b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Sms.Tencent/LINGYUN/Abp/Sms/Tencent/TencentCloudSmsSender.cs new file mode 100644 index 000000000..fb10946b1 --- /dev/null +++ b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Sms.Tencent/LINGYUN/Abp/Sms/Tencent/TencentCloudSmsSender.cs @@ -0,0 +1,97 @@ +using LINGYUN.Abp.Sms.Tencent.Settings; +using LINGYUN.Abp.Tencent; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using TencentCloud.Sms.V20210111; +using TencentCloud.Sms.V20210111.Models; +using Volo.Abp; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Json; +using Volo.Abp.Settings; +using Volo.Abp.Sms; + +namespace LINGYUN.Abp.Sms.Tencent +{ + [Dependency(ReplaceServices = true)] + public class TencentCloudSmsSender : ISmsSender, ITransientDependency + { + public ILogger Logger { protected get; set; } + + protected IJsonSerializer JsonSerializer { get; } + protected ISettingProvider SettingProvider { get; } + protected IServiceProvider ServiceProvider { get; } + protected TencentCloudClientFactory TencentCloudClientFactory { get; } + public TencentCloudSmsSender( + IJsonSerializer jsonSerializer, + ISettingProvider settingProvider, + IServiceProvider serviceProvider, + TencentCloudClientFactory tencentCloudClientFactory) + { + JsonSerializer = jsonSerializer; + SettingProvider = settingProvider; + ServiceProvider = serviceProvider; + TencentCloudClientFactory = tencentCloudClientFactory; + + Logger = NullLogger.Instance; + } + + public virtual async Task SendAsync(SmsMessage smsMessage) + { + var appId = await SettingProvider.GetOrNullAsync(TencentCloudSmsSettingNames.AppId); + + Check.NotNullOrWhiteSpace(appId, TencentCloudSmsSettingNames.AppId); + + // 统一使用 TemplateCode作为模板参数, 解决不一样的sms提供商参数差异 + if (!smsMessage.Properties.TryGetValue("TemplateCode", out var templateId)) + { + templateId = await SettingProvider.GetOrNullAsync(TencentCloudSmsSettingNames.DefaultTemplateId); + } + + if (!smsMessage.Properties.TryGetValue("SignName", out var signName)) + { + signName = await SettingProvider.GetOrNullAsync(TencentCloudSmsSettingNames.DefaultSignName); + } + + if (!smsMessage.Properties.TryGetValue("TemplateParam", out var templateParam)) + { + templateParam = await SettingProvider.GetOrNullAsync(TencentCloudSmsSettingNames.DefaultSignName); + } + + var smsClient = await TencentCloudClientFactory.CreateAsync(); + + var request = new SendSmsRequest + { + SmsSdkAppId = appId, + SignName = signName?.ToString(), + TemplateId = templateId?.ToString(), + PhoneNumberSet = smsMessage.PhoneNumber.Split(';'), + TemplateParamSet = templateParam?.ToString()?.Split(';'), + }; + + var response = await smsClient.SendSms(request); + + var warningMessage = response.SendStatusSet + .Where(x => !"ok".Equals(x.Code, StringComparison.InvariantCultureIgnoreCase)) + // 记录流水号, 手机号, 错误代码, 错误信息 + .Select(x => $"SerialNo: {x.SerialNo}, PhoneNumber: {x.PhoneNumber}, Status: {x.Code}, Error: {x.Message}"); + // 所有短信发送失败, 抛出错误 + // 只要一条发送成功,即视为成功 + if (!response.SendStatusSet.Any(x => "ok".Equals(x.Code, StringComparison.InvariantCultureIgnoreCase))) + { + var errorMessage = warningMessage.JoinAsString(Environment.NewLine); + + throw new AbpException($"Send tencent cloud sms error: {errorMessage}"); + } + + if (warningMessage.Any()) + { + Logger.LogWarning("Some failed send sms messages, error info:"); + Logger.LogWarning(warningMessage.JoinAsString(Environment.NewLine)); + } + } + } +} diff --git a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Sms.Tencent/Volo/Abp/Sms/TencentSmsSenderExtensions.cs b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Sms.Tencent/Volo/Abp/Sms/TencentSmsSenderExtensions.cs new file mode 100644 index 000000000..799a2e6a3 --- /dev/null +++ b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Sms.Tencent/Volo/Abp/Sms/TencentSmsSenderExtensions.cs @@ -0,0 +1,58 @@ +using LINGYUN.Abp.Sms.Tencent; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Volo.Abp.Sms +{ + public static class TencentSmsSenderExtensions + { + /// + /// 扩展短信接口 + /// + /// + /// 短信模板号 + /// 发送手机号 + /// 短信模板参数 + /// + public static async Task SendAsync( + this ISmsSender smsSender, + string templateCode, + string phoneNumber, + IDictionary templateParams = null) + { + var smsMessage = new SmsMessage(phoneNumber, nameof(TencentCloudSmsSender)); + smsMessage.Properties.Add("TemplateCode", templateCode); + if(templateParams != null) + { + smsMessage.Properties.Add("TemplateParam", templateParams); + } + await smsSender.SendAsync(smsMessage); + } + + /// + /// 扩展短信接口 + /// + /// + /// 短信签名 + /// 短信模板号 + /// 发送手机号 + /// 短信模板参数 + /// + public static async Task SendAsync( + this ISmsSender smsSender, + string signName, + string templateCode, + string phoneNumber, + IDictionary templateParams = null) + { + var smsMessage = new SmsMessage(phoneNumber, nameof(TencentCloudSmsSender)); + smsMessage.Properties.Add("SignName", signName); + smsMessage.Properties.Add("TemplateCode", templateCode); + if (templateParams != null) + { + smsMessage.Properties.Add("TemplateParam", templateParams); + } + await smsSender.SendAsync(smsMessage); + } + } +} diff --git a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent.SettingManagement/LINGYUN.Abp.Tencent.SettingManagement.csproj b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent.SettingManagement/LINGYUN.Abp.Tencent.SettingManagement.csproj new file mode 100644 index 000000000..1b20b4cf4 --- /dev/null +++ b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent.SettingManagement/LINGYUN.Abp.Tencent.SettingManagement.csproj @@ -0,0 +1,26 @@ + + + + + + + net6.0 + + + + + + + + + + + + + + + + + + + diff --git a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent.SettingManagement/LINGYUN/Abp/Tencent/SettingManagement/AbpTencentCloudSettingManagementModule.cs b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent.SettingManagement/LINGYUN/Abp/Tencent/SettingManagement/AbpTencentCloudSettingManagementModule.cs new file mode 100644 index 000000000..a15337dca --- /dev/null +++ b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent.SettingManagement/LINGYUN/Abp/Tencent/SettingManagement/AbpTencentCloudSettingManagementModule.cs @@ -0,0 +1,16 @@ +using LINGYUN.Abp.SettingManagement; +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.Modularity; +using Volo.Abp.SettingManagement; + +namespace LINGYUN.Abp.Tencent.SettingManagement; + +[DependsOn( + typeof(AbpTencentCloudModule), + typeof(AbpSettingManagementApplicationContractsModule), + typeof(AbpSettingManagementDomainModule), + typeof(AbpAspNetCoreMvcModule))] +public class AbpTencentCloudSettingManagementModule : AbpModule +{ + +} \ No newline at end of file diff --git a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent.SettingManagement/LINGYUN/Abp/Tencent/SettingManagement/ITenantCloudSettingAppService.cs b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent.SettingManagement/LINGYUN/Abp/Tencent/SettingManagement/ITenantCloudSettingAppService.cs new file mode 100644 index 000000000..ca935832e --- /dev/null +++ b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent.SettingManagement/LINGYUN/Abp/Tencent/SettingManagement/ITenantCloudSettingAppService.cs @@ -0,0 +1,7 @@ +using LINGYUN.Abp.SettingManagement; + +namespace LINGYUN.Abp.Tencent.SettingManagement; + +public interface ITenantCloudSettingAppService : IReadonlySettingAppService +{ +} diff --git a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent.SettingManagement/LINGYUN/Abp/Tencent/SettingManagement/TenantCloudSettingAppService.cs b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent.SettingManagement/LINGYUN/Abp/Tencent/SettingManagement/TenantCloudSettingAppService.cs new file mode 100644 index 000000000..8fd8df93e --- /dev/null +++ b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent.SettingManagement/LINGYUN/Abp/Tencent/SettingManagement/TenantCloudSettingAppService.cs @@ -0,0 +1,168 @@ +using LINGYUN.Abp.SettingManagement; +using LINGYUN.Abp.Sms.Tencent.Settings; +using LINGYUN.Abp.Tencent.Localization; +using LINGYUN.Abp.Tencent.Settings; +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.Application.Services; +using Volo.Abp.Authorization.Permissions; +using Volo.Abp.MultiTenancy; +using Volo.Abp.SettingManagement; +using Volo.Abp.Settings; +using ValueType = LINGYUN.Abp.SettingManagement.ValueType; + +namespace LINGYUN.Abp.Tencent.SettingManagement; + +public class TenantCloudSettingAppService : ApplicationService, ITenantCloudSettingAppService +{ + protected ISettingManager SettingManager { get; } + protected IPermissionChecker PermissionChecker { get; } + protected ISettingDefinitionManager SettingDefinitionManager { get; } + protected ITencentCloudClientFactory TencentCloudClientFactory { get; } + + public TenantCloudSettingAppService( + ISettingManager settingManager, + IPermissionChecker permissionChecker, + ISettingDefinitionManager settingDefinitionManager, + ITencentCloudClientFactory tencentCloudClientFactory) + { + SettingManager = settingManager; + PermissionChecker = permissionChecker; + SettingDefinitionManager = settingDefinitionManager; + TencentCloudClientFactory = tencentCloudClientFactory; + + LocalizationResource = typeof(TencentCloudResource); + } + + public virtual async Task GetAllForCurrentTenantAsync() + { + return await GetAllForProviderAsync(TenantSettingValueProvider.ProviderName, CurrentTenant.GetId().ToString()); + } + + public virtual async Task GetAllForGlobalAsync() + { + return await GetAllForProviderAsync(GlobalSettingValueProvider.ProviderName, null); + } + + protected virtual async Task GetAllForProviderAsync(string providerName, string providerKey) + { + var settingGroups = new SettingGroupResult(); + + // 无权限返回空结果,直接报错的话,网关聚合会抛出异常 + if (await PermissionChecker.IsGrantedAsync(TenantCloudSettingPermissionNames.Settings)) + { + var settingGroup = new SettingGroupDto(L["DisplayName:TenantCloud"], L["DisplayName:TenantCloud"]); + + #region 基本设置 + + var basicSetting = settingGroup.AddSetting(L["DisplayName:TenantCloud.BasicSetting"], L["Description:TenantCloud.BasicSetting"]); + + basicSetting.AddDetail( + SettingDefinitionManager.Get(TencentCloudSettingNames.EndPoint), + StringLocalizerFactory, + await SettingManager.GetOrNullAsync(TencentCloudSettingNames.EndPoint, providerName, providerKey), + ValueType.Option, + providerName) + .AddOptions(GetAvailableRegionOptions()); + basicSetting.AddDetail( + SettingDefinitionManager.Get(TencentCloudSettingNames.SecretId), + StringLocalizerFactory, + await SettingManager.GetOrNullAsync(TencentCloudSettingNames.SecretId, providerName, providerKey), + ValueType.String, + providerName); + basicSetting.AddDetail( + SettingDefinitionManager.Get(TencentCloudSettingNames.SecretKey), + StringLocalizerFactory, + await SettingManager.GetOrNullAsync(TencentCloudSettingNames.SecretKey, providerName, providerKey), + ValueType.String, + providerName); + + #endregion + + #region 连接信息 + + var connectionSetting = settingGroup.AddSetting( + L["DisplayName:TenantCloud.ConnectionSetting"], L["Description:TenantCloud.ConnectionSetting"]); + + connectionSetting.AddDetail( + SettingDefinitionManager.Get(TencentCloudSettingNames.Connection.HttpMethod), + StringLocalizerFactory, + await SettingManager.GetOrNullAsync(TencentCloudSettingNames.Connection.HttpMethod, providerName, providerKey), + ValueType.Option, + providerName) + .AddOption("POST", "POST") + .AddOption("GET", "GET"); + connectionSetting.AddDetail( + SettingDefinitionManager.Get(TencentCloudSettingNames.Connection.Timeout), + StringLocalizerFactory, + await SettingManager.GetOrNullAsync(TencentCloudSettingNames.Connection.Timeout, providerName, providerKey), + ValueType.Number, + providerName); + connectionSetting.AddDetail( + SettingDefinitionManager.Get(TencentCloudSettingNames.Connection.WebProxy), + StringLocalizerFactory, + await SettingManager.GetOrNullAsync(TencentCloudSettingNames.Connection.WebProxy, providerName, providerKey), + ValueType.Number, + providerName); + + #endregion + + #region 短信设置 + + var smsSetting = settingGroup.AddSetting( + L["DisplayName:TenantCloud.SmsSetting"], L["Description:TenantCloud.SmsSetting"]); + + smsSetting.AddDetail( + SettingDefinitionManager.Get(TencentCloudSmsSettingNames.AppId), + StringLocalizerFactory, + await SettingManager.GetOrNullAsync(TencentCloudSmsSettingNames.AppId, providerName, providerKey), + ValueType.String, + providerName); + smsSetting.AddDetail( + SettingDefinitionManager.Get(TencentCloudSmsSettingNames.DefaultTemplateId), + StringLocalizerFactory, + await SettingManager.GetOrNullAsync(TencentCloudSmsSettingNames.DefaultTemplateId, providerName, providerKey), + ValueType.String, + providerName); + smsSetting.AddDetail( + SettingDefinitionManager.Get(TencentCloudSmsSettingNames.DefaultSignName), + StringLocalizerFactory, + await SettingManager.GetOrNullAsync(TencentCloudSmsSettingNames.DefaultSignName, providerName, providerKey), + ValueType.String, + providerName); + + #endregion + + settingGroups.AddGroup(settingGroup); + } + + return settingGroups; + } + + protected virtual IEnumerable GetAvailableRegionOptions() + { + return new OptionDto[] + { + new OptionDto(L["Region:Beijing"], "ap-beijing"), + new OptionDto(L["Region:Chengdu"], "ap-chengdu"), + new OptionDto(L["Region:Chongqing"], "ap-chongqing"), + new OptionDto(L["Region:Guangzhou"], "ap-guangzhou"), + new OptionDto(L["Region:Hongkong"], "ap-hongkong"), + new OptionDto(L["Region:Nanjing"], "ap-nanjing"), + new OptionDto(L["Region:Shanghai"], "ap-shanghai"), + new OptionDto(L["Region:ShanghaiFsi"], "ap-shanghai-fsi"), + new OptionDto(L["Region:ShenzhenFsi"], "ap-shenzhen-fsi"), + new OptionDto(L["Region:Bangkok"], "ap-bangkok"), + new OptionDto(L["Region:Jakarta"], "ap-jakarta"), + new OptionDto(L["Region:Mumbai"], "ap-mumbai"), + new OptionDto(L["Region:Seoul"], "ap-seoul"), + new OptionDto(L["Region:Singapore"], "ap-singapore"), + new OptionDto(L["Region:Tokyo"], "ap-tokyo"), + new OptionDto(L["Region:Frankfurt"], "eu-frankfurt"), + new OptionDto(L["Region:Moscow"], "eu-moscow"), + new OptionDto(L["Region:Virginia"], "na-ashburn"), + new OptionDto(L["Region:SiliconValley"], "na-siliconvalley"), + new OptionDto(L["Region:Toronto"], "na-toronto"), + }; + } +} diff --git a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent.SettingManagement/LINGYUN/Abp/Tencent/SettingManagement/TenantCloudSettingPermissionDefinitionProvider.cs b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent.SettingManagement/LINGYUN/Abp/Tencent/SettingManagement/TenantCloudSettingPermissionDefinitionProvider.cs new file mode 100644 index 000000000..4fa39af5c --- /dev/null +++ b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent.SettingManagement/LINGYUN/Abp/Tencent/SettingManagement/TenantCloudSettingPermissionDefinitionProvider.cs @@ -0,0 +1,23 @@ +using LINGYUN.Abp.Tencent.Localization; +using Volo.Abp.Authorization.Permissions; +using Volo.Abp.Localization; + +namespace LINGYUN.Abp.Tencent.SettingManagement; + +public class TenantCloudSettingPermissionDefinitionProvider : PermissionDefinitionProvider +{ + public override void Define(IPermissionDefinitionContext context) + { + var wechatGroup = context.AddGroup( + TenantCloudSettingPermissionNames.GroupName, + L("Permission:TencentCloud")); + + wechatGroup.AddPermission( + TenantCloudSettingPermissionNames.Settings, L("Permission:TencentCloud.Settings")); + } + + protected LocalizableString L(string name) + { + return LocalizableString.Create(name); + } +} diff --git a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent.SettingManagement/LINGYUN/Abp/Tencent/SettingManagement/TenantCloudSettingPermissionNames.cs b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent.SettingManagement/LINGYUN/Abp/Tencent/SettingManagement/TenantCloudSettingPermissionNames.cs new file mode 100644 index 000000000..9e2f09e80 --- /dev/null +++ b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent.SettingManagement/LINGYUN/Abp/Tencent/SettingManagement/TenantCloudSettingPermissionNames.cs @@ -0,0 +1,9 @@ +namespace LINGYUN.Abp.Tencent.SettingManagement +{ + public class TenantCloudSettingPermissionNames + { + public const string GroupName = "Abp.Tencent"; + + public const string Settings = GroupName + ".Settings"; + } +} diff --git a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent/LINGYUN.Abp.Tencent.csproj b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent/LINGYUN.Abp.Tencent.csproj index d5aea83db..aa3675404 100644 --- a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent/LINGYUN.Abp.Tencent.csproj +++ b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent/LINGYUN.Abp.Tencent.csproj @@ -10,19 +10,19 @@ - - + - - + + + diff --git a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent/LINYUN/Abp/Tencent/AbpTencentCloudModule.cs b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent/LINYUN/Abp/Tencent/AbpTencentCloudModule.cs index 7af09c44e..9a0ccd660 100644 --- a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent/LINYUN/Abp/Tencent/AbpTencentCloudModule.cs +++ b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent/LINYUN/Abp/Tencent/AbpTencentCloudModule.cs @@ -1,12 +1,15 @@ -using LINYUN.Abp.Tencent.Localization; +using LINGYUN.Abp.Tencent.Localization; +using Volo.Abp.Caching; using Volo.Abp.Json; using Volo.Abp.Localization; using Volo.Abp.Modularity; using Volo.Abp.VirtualFileSystem; +using Microsoft.Extensions.DependencyInjection; namespace LINGYUN.Abp.Tencent { [DependsOn( + typeof(AbpCachingModule), typeof(AbpJsonModule), typeof(AbpLocalizationModule))] public class AbpTencentCloudModule : AbpModule @@ -21,9 +24,18 @@ namespace LINGYUN.Abp.Tencent Configure(options => { options.Resources - .Add() + .Add() .AddVirtualJson("/LINGYUN/Abp/Tencent/Localization/Resources"); }); + + context.Services.AddTransient>(); + //Configure(options => + //{ + // 按照腾讯SDK, 所有客户端都有三个参数组成, 暂时设计为通过反射构造函数来创建客户端 + // options.ClientProxies.Add(typeof(AaClient), (cred, endpoint, profile) => new AaClient(cred, endpoint, profile)); + // options.ClientProxies.Add(typeof(AaiClient), (cred, endpoint, profile) => new AaiClient(cred, endpoint, profile)); + // options.ClientProxies.Add(typeof(AdvisorClient), (cred, endpoint, profile) => new AdvisorClient(cred, endpoint, profile)); + //}); } } } diff --git a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent/LINYUN/Abp/Tencent/AbpTencentCloudOptions.cs b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent/LINYUN/Abp/Tencent/AbpTencentCloudOptions.cs new file mode 100644 index 000000000..3ff855a67 --- /dev/null +++ b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent/LINYUN/Abp/Tencent/AbpTencentCloudOptions.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using TencentCloud.Common; +using TencentCloud.Common.Profile; + +namespace LINGYUN.Abp.Tencent; + +public class AbpTencentCloudOptions +{ + public IDictionary> ClientProxies { get; } + + public AbpTencentCloudOptions() + { + ClientProxies = new Dictionary>(); + } +} diff --git a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent/LINYUN/Abp/Tencent/AbstractTencentCloudClientFactory.cs b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent/LINYUN/Abp/Tencent/AbstractTencentCloudClientFactory.cs new file mode 100644 index 000000000..235914c36 --- /dev/null +++ b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent/LINYUN/Abp/Tencent/AbstractTencentCloudClientFactory.cs @@ -0,0 +1,124 @@ +using LINGYUN.Abp.Tencent.Settings; +using Microsoft.Extensions.Caching.Memory; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Settings; + +namespace LINGYUN.Abp.Tencent; + +public abstract class AbstractTencentCloudClientFactory +{ + protected IMemoryCache ClientCache { get; } + protected ICurrentTenant CurrentTenant { get; } + protected ISettingProvider SettingProvider { get; } + + public AbstractTencentCloudClientFactory( + IMemoryCache clientCache, + ICurrentTenant currentTenant, + ISettingProvider settingProvider) + { + ClientCache = clientCache; + CurrentTenant = currentTenant; + SettingProvider = settingProvider; + } + + public virtual async Task CreateAsync() + { + var clientCacheItem = await GetClientCacheItemAsync(); + + return CreateClient(clientCacheItem); + } + + protected abstract TClient CreateClient(TencentCloudClientCacheItem cloudCache); + + protected virtual async Task GetClientCacheItemAsync() + { + return await ClientCache.GetOrCreateAsync( + TencentCloudClientCacheItem.CalculateCacheKey(CurrentTenant), + async (_) => + { + var secretId = await SettingProvider.GetOrNullAsync(TencentCloudSettingNames.SecretId); + var secretKey = await SettingProvider.GetOrNullAsync(TencentCloudSettingNames.SecretKey); + var endpoint = await SettingProvider.GetOrNullAsync(TencentCloudSettingNames.EndPoint); + + Check.NotNullOrWhiteSpace(secretId, TencentCloudSettingNames.SecretId); + Check.NotNullOrWhiteSpace(secretKey, TencentCloudSettingNames.SecretKey); + + + var method = await SettingProvider.GetOrNullAsync(TencentCloudSettingNames.Connection.HttpMethod); + var webProxy = await SettingProvider.GetOrNullAsync(TencentCloudSettingNames.Connection.WebProxy); + var timeout = await SettingProvider.GetAsync(TencentCloudSettingNames.Connection.Timeout, 60); + + return new TencentCloudClientCacheItem + { + SecretId = secretId, + SecretKey = secretKey, + // 连接区域 + EndPoint = endpoint, + HttpMethod = method, + WebProxy = webProxy, + Timeout = timeout, + }; + }); + } +} + +public abstract class AbstractTencentCloudClientFactory +{ + protected IMemoryCache ClientCache { get; } + protected ICurrentTenant CurrentTenant { get; } + protected ISettingProvider SettingProvider { get; } + + public AbstractTencentCloudClientFactory( + IMemoryCache clientCache, + ICurrentTenant currentTenant, + ISettingProvider settingProvider) + { + ClientCache = clientCache; + CurrentTenant = currentTenant; + SettingProvider = settingProvider; + } + + public virtual async Task CreateAsync(TConfiguration configuration) + { + var clientCacheItem = await GetClientCacheItemAsync(); + + return CreateClient(configuration, clientCacheItem); + } + + protected abstract TClient CreateClient(TConfiguration configuration, TencentCloudClientCacheItem cloudCache); + + protected virtual async Task GetClientCacheItemAsync() + { + return await ClientCache.GetOrCreateAsync( + TencentCloudClientCacheItem.CalculateCacheKey(CurrentTenant), + async (_) => + { + var secretId = await SettingProvider.GetOrNullAsync(TencentCloudSettingNames.SecretId); + var secretKey = await SettingProvider.GetOrNullAsync(TencentCloudSettingNames.SecretKey); + var endpoint = await SettingProvider.GetOrNullAsync(TencentCloudSettingNames.EndPoint); + var durationSecond = await SettingProvider.GetAsync(TencentCloudSettingNames.DurationSecond, 3600); + + Check.NotNullOrWhiteSpace(secretId, TencentCloudSettingNames.SecretId); + Check.NotNullOrWhiteSpace(secretKey, TencentCloudSettingNames.SecretKey); + + + var method = await SettingProvider.GetOrNullAsync(TencentCloudSettingNames.Connection.HttpMethod); + var webProxy = await SettingProvider.GetOrNullAsync(TencentCloudSettingNames.Connection.WebProxy); + var timeout = await SettingProvider.GetAsync(TencentCloudSettingNames.Connection.Timeout, 60); + + return new TencentCloudClientCacheItem + { + SecretId = secretId, + SecretKey = secretKey, + // 连接区域 + EndPoint = endpoint, + DurationSecond = durationSecond, + HttpMethod = method, + WebProxy = webProxy, + Timeout = timeout, + }; + }); + } +} diff --git a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent/LINYUN/Abp/Tencent/Localization/Resources/en.json b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent/LINYUN/Abp/Tencent/Localization/Resources/en.json index a9c8dcc3f..2dcca815b 100644 --- a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent/LINYUN/Abp/Tencent/Localization/Resources/en.json +++ b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent/LINYUN/Abp/Tencent/Localization/Resources/en.json @@ -1,5 +1,42 @@ { "culture": "en", "texts": { + "DisplayName:TenantCloud": "Tenant Cloud", + "DisplayName:TenantCloud.BasicSetting": "Basic Setting", + "Description:TenantCloud.BasicSetting": "Tencent cloud basic information configuration", + "DisplayName:TenantCloud.ConnectionSetting": "Connection Setting", + "Description:TenantCloud.ConnectionSetting": "Tencent Cloud SDK connection configuration", + "DisplayName:EndPoint": "EndPoint", + "Description:EndPoint": "Represents a region where a resource resides, and each region contains one or more available regions.", + "DisplayName:SecretId": "Secret Id", + "Description:SecretId": "The Secret Id can be obtained only from Tencent Cloud Management Console.", + "DisplayName:SecretKey": "Secret Key", + "Description:SecretKey": "The Secret Key can be obtained only from Tencent Cloud Management Console.", + "DisplayName:HttpMethod": "Http Method", + "Description:HttpMethod": "The SDK uses the POST method by default, but if you must use the GET method, you can set it here. The GET method cannot handle some large requests.", + "DisplayName:Timeout": "Timeout", + "Description:Timeout": "Connection timeout, in seconds (default: 60 seconds)", + "DisplayName:WebProxy": "Web Proxy", + "Description:WebProxy": "Proxy server, set when there are proxy servers in your environment.", + "Region:Beijing": "North China (Beijing)", + "Region:Chengdu": "Southwest China (Chengdu)", + "Region:Chongqing": "Southwest China (Chongqing)", + "Region:Guangzhou": "South China (Guangzhou)", + "Region:Hongkong": "Hong Kong/Macao/Taiwan (Hong Kong, China)", + "Region:Nanjing": "East China (Nanjing)", + "Region:Shanghai": "East China (Shanghai)", + "Region:ShanghaiFsi": "East China (Shanghai financial)", + "Region:ShenzhenFsi": "South China (Shenzhen financial)", + "Region:Bangkok": "Southeast Asia (Bangkok)", + "Region:Jakarta": "Southeast Asia (Jakarta)", + "Region:Mumbai": "South Asia (Mumbai)", + "Region:Seoul": "Northeast Asia (Seoul)", + "Region:Singapore": "Southeast Asia (Singapore)", + "Region:Tokyo": "Northeast Asia (Tokyo)", + "Region:Frankfurt": "Europe (Frankfurt)", + "Region:Moscow": "Europe (Moscow)", + "Region:Virginia": "Eastern US (Virginia)", + "Region:SiliconValley": "Western US (Silicon Valley)", + "Region:Toronto": "North America (Toronto)" } } \ No newline at end of file diff --git a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent/LINYUN/Abp/Tencent/Localization/Resources/zh-Hans.json b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent/LINYUN/Abp/Tencent/Localization/Resources/zh-Hans.json index c5ad81326..4b76d952b 100644 --- a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent/LINYUN/Abp/Tencent/Localization/Resources/zh-Hans.json +++ b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent/LINYUN/Abp/Tencent/Localization/Resources/zh-Hans.json @@ -1,5 +1,42 @@ { "culture": "zh-Hans", "texts": { + "DisplayName:TenantCloud": "腾讯云服务", + "DisplayName:TenantCloud.BasicSetting": "基础配置", + "Description:TenantCloud.BasicSetting": "腾讯云基础信息配置", + "DisplayName:TenantCloud.ConnectionSetting": "连接配置", + "Description:TenantCloud.ConnectionSetting": "腾讯云SDK连接相关配置", + "DisplayName:EndPoint": "资源地域", + "Description:EndPoint": "表示资源所在的地域,每个地域包含一个或多个可用区。", + "DisplayName:SecretId": "密钥Id", + "Description:SecretId": "密钥Id,只能从腾讯云管理控制台获取。", + "DisplayName:SecretKey": "密钥Key", + "Description:SecretKey": "密钥Key,只能从腾讯云管理控制台获取。", + "DisplayName:HttpMethod": "请求类型", + "Description:HttpMethod": "SDK默认使用POST方法,如果你一定要使用GET方法,可以在这里设置。GET方法无法处理一些较大的请求。", + "DisplayName:Timeout": "超时时间", + "Description:Timeout": "请求连接超时时间,单位为秒(默认60秒)", + "DisplayName:WebProxy": "代理服务器", + "Description:WebProxy": "代理服务器,当你的环境下有代理服务器时设定", + "Region:Beijing": "华北地区(北京)", + "Region:Chengdu": "西南地区(成都)", + "Region:Chongqing": "西南地区(重庆)", + "Region:Guangzhou": "华南地区(广州)", + "Region:Hongkong": "港澳台地区(中国香港)", + "Region:Nanjing": "华东地区(南京)", + "Region:Shanghai": "华东地区(上海)", + "Region:ShanghaiFsi": "华东地区(上海金融)", + "Region:ShenzhenFsi": "华南地区(深圳金融)", + "Region:Bangkok": "亚太东南(曼谷)", + "Region:Jakarta": "亚太东南(雅加达)", + "Region:Mumbai": "亚太南部(孟买)", + "Region:Seoul": "亚太东北(首尔)", + "Region:Singapore": "亚太东南(新加坡)", + "Region:Tokyo": "亚太东北(东京)", + "Region:Frankfurt": "欧洲地区(法兰克福)", + "Region:Moscow": "欧洲地区(莫斯科)", + "Region:Virginia": "美国东部(弗吉尼亚)", + "Region:SiliconValley": "美国西部(硅谷)", + "Region:Toronto": "北美地区(多伦多)" } } \ No newline at end of file diff --git a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent/LINYUN/Abp/Tencent/Localization/TencentCloudResource.cs b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent/LINYUN/Abp/Tencent/Localization/TencentCloudResource.cs new file mode 100644 index 000000000..160fcbc6f --- /dev/null +++ b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent/LINYUN/Abp/Tencent/Localization/TencentCloudResource.cs @@ -0,0 +1,9 @@ +using Volo.Abp.Localization; + +namespace LINGYUN.Abp.Tencent.Localization +{ + [LocalizationResourceName("TencentCloud")] + public class TencentCloudResource + { + } +} diff --git a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent/LINYUN/Abp/Tencent/Localization/TencentResource.cs b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent/LINYUN/Abp/Tencent/Localization/TencentResource.cs deleted file mode 100644 index cc839f75a..000000000 --- a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent/LINYUN/Abp/Tencent/Localization/TencentResource.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Volo.Abp.Localization; - -namespace LINYUN.Abp.Tencent.Localization -{ - [LocalizationResourceName("Tencent")] - public class TencentResource - { - } -} diff --git a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent/LINYUN/Abp/Tencent/Settings/TencentCloudSettingDefinitionProvider.cs b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent/LINYUN/Abp/Tencent/Settings/TencentCloudSettingDefinitionProvider.cs new file mode 100644 index 000000000..4eef82366 --- /dev/null +++ b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent/LINYUN/Abp/Tencent/Settings/TencentCloudSettingDefinitionProvider.cs @@ -0,0 +1,94 @@ +using LINGYUN.Abp.Tencent.Localization; +using LINGYUN.Abp.Tencent.Settings; +using Volo.Abp.Localization; +using Volo.Abp.Settings; + +namespace LINYUN.Abp.Tencent.Settings; + +public class TencentCloudSettingDefinitionProvider : SettingDefinitionProvider +{ + public override void Define(ISettingDefinitionContext context) + { + context.Add(CreateTencentCloudSettings()); + } + + private SettingDefinition[] CreateTencentCloudSettings() + { + return new SettingDefinition[] + { + new SettingDefinition( + TencentCloudSettingNames.EndPoint, + // 腾讯云默认使用广州区域 + defaultValue: "ap-guangzhou", + displayName: L("DisplayName:EndPoint"), + description: L("Description:EndPoint"), + isVisibleToClients: false + ) + .WithProviders( + DefaultValueSettingValueProvider.ProviderName, + GlobalSettingValueProvider.ProviderName, + TenantSettingValueProvider.ProviderName), + new SettingDefinition( + TencentCloudSettingNames.SecretId, + displayName: L("DisplayName:SecretId"), + description: L("Description:SecretId"), + isVisibleToClients: false, + isEncrypted: true + ) + .WithProviders( + DefaultValueSettingValueProvider.ProviderName, + GlobalSettingValueProvider.ProviderName, + TenantSettingValueProvider.ProviderName), + new SettingDefinition( + TencentCloudSettingNames.SecretKey, + displayName: L("DisplayName:SecretKey"), + description: L("Description:SecretKey"), + isVisibleToClients: false, + isEncrypted: true + ) + .WithProviders( + DefaultValueSettingValueProvider.ProviderName, + GlobalSettingValueProvider.ProviderName, + TenantSettingValueProvider.ProviderName), + new SettingDefinition( + TencentCloudSettingNames.Connection.HttpMethod, + // 默认 post + defaultValue: "POST", + displayName: L("DisplayName:HttpMethod"), + description: L("Description:HttpMethod"), + isVisibleToClients: false + ) + .WithProviders( + DefaultValueSettingValueProvider.ProviderName, + GlobalSettingValueProvider.ProviderName, + TenantSettingValueProvider.ProviderName), + new SettingDefinition( + TencentCloudSettingNames.Connection.Timeout, + // 默认 60秒 + defaultValue: "60", + displayName: L("DisplayName:Timeout"), + description: L("Description:Timeout"), + isVisibleToClients: false + ) + .WithProviders( + DefaultValueSettingValueProvider.ProviderName, + GlobalSettingValueProvider.ProviderName, + TenantSettingValueProvider.ProviderName), + new SettingDefinition( + TencentCloudSettingNames.Connection.WebProxy, + displayName: L("DisplayName:WebProxy"), + description: L("Description:WebProxy"), + isVisibleToClients: false + ) + .WithProviders( + DefaultValueSettingValueProvider.ProviderName, + GlobalSettingValueProvider.ProviderName, + TenantSettingValueProvider.ProviderName), + }; + } + + private ILocalizableString L(string name) + { + return LocalizableString.Create(name); + } +} diff --git a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent/LINYUN/Abp/Tencent/Settings/TencentCloudSettingNames.cs b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent/LINYUN/Abp/Tencent/Settings/TencentCloudSettingNames.cs new file mode 100644 index 000000000..944713db2 --- /dev/null +++ b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent/LINYUN/Abp/Tencent/Settings/TencentCloudSettingNames.cs @@ -0,0 +1,47 @@ +namespace LINGYUN.Abp.Tencent.Settings; + +public static class TencentCloudSettingNames +{ + public const string Prefix = "Abp.TencentCloud"; + /// + /// SecretId + /// + public const string SecretId = Prefix + ".SecretId"; + /// + /// SecretKey + /// + public const string SecretKey = Prefix + ".SecretKey"; + /// + /// 连接地域 + /// + public const string EndPoint = Prefix + ".EndPoint"; + /// + /// 会话持续时间 + /// + public const string DurationSecond = Prefix + ".DurationSecond"; + /// + /// 连接设置 + /// + public class Connection + { + public const string Prefix = TencentCloudSettingNames.Prefix + ".Connection"; + /// + /// 代理服务器 + /// + public const string WebProxy = Prefix + ".WebProxy"; + /// + /// 连接地域 + /// 按照腾讯云文档,对于金融区服务需要指定域名 + /// + public const string EndPoint = Prefix + ".EndPoint"; + /// + /// Http方法,按照腾讯云文档,不同的方法对于请求大小有限制 + /// 默认:POST + /// + public const string HttpMethod = Prefix + ".HttpMethod"; + /// + /// 超时时间 + /// + public const string Timeout = Prefix + ".Timeout"; + } +} diff --git a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent/LINYUN/Abp/Tencent/TencentCloudClientCacheItem.cs b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent/LINYUN/Abp/Tencent/TencentCloudClientCacheItem.cs new file mode 100644 index 000000000..16a353e99 --- /dev/null +++ b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent/LINYUN/Abp/Tencent/TencentCloudClientCacheItem.cs @@ -0,0 +1,21 @@ +using Volo.Abp.MultiTenancy; + +namespace LINGYUN.Abp.Tencent; + +public class TencentCloudClientCacheItem +{ + public const string CacheKeyFormat = "pn:{0},n:tenant-cloud"; + public string SecretId { get; set; } + public string SecretKey { get; set; } + public string EndPoint { get; set; } + public string WebProxy { get; set; } + public string ApiEndPoint { get; set; } + public string HttpMethod { get; set; } + public int Timeout { get; set; } + public int DurationSecond { get; set; } + + public static string CalculateCacheKey(ICurrentTenant currentTenant) + { + return string.Format(CacheKeyFormat, currentTenant.IsAvailable ? currentTenant.GetId().ToString() : "global"); + } +} diff --git a/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent/LINYUN/Abp/Tencent/TencentCloudClientFactory.cs b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent/LINYUN/Abp/Tencent/TencentCloudClientFactory.cs new file mode 100644 index 000000000..6d9bfd7ba --- /dev/null +++ b/aspnet-core/modules/cloud-tencent/LINGYUN.Abp.Tencent/LINYUN/Abp/Tencent/TencentCloudClientFactory.cs @@ -0,0 +1,55 @@ +using Microsoft.Extensions.Caching.Memory; +using System.Linq; +using TencentCloud.Common; +using TencentCloud.Common.Profile; +using Volo.Abp; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Settings; + +namespace LINGYUN.Abp.Tencent; + +public class TencentCloudClientFactory : AbstractTencentCloudClientFactory +{ + public TencentCloudClientFactory( + IMemoryCache clientCache, + ICurrentTenant currentTenant, + ISettingProvider settingProvider) + : base(clientCache, currentTenant, settingProvider) + { + } + + protected override TClient CreateClient(TencentCloudClientCacheItem cloudCache) + { + var clientCtr = typeof(TClient) + .GetConstructors() + .Where(x => x.GetParameters().Length == 3) + .FirstOrDefault(); + if (clientCtr != null) + { + var cred = new Credential + { + SecretId = cloudCache.SecretId, + SecretKey = cloudCache.SecretKey, + }; + + var httpProfile = new HttpProfile + { + ReqMethod = cloudCache.HttpMethod ?? "POST", + Timeout = cloudCache.Timeout, + // 不同的api需要的区域不同, 有的 + Endpoint = cloudCache.ApiEndPoint, + WebProxy = cloudCache.WebProxy, + }; + var clientProfile = new ClientProfile + { + HttpProfile = httpProfile, + }; + + // 通过反射创建客户端实例 + // TODO: 如果影响到性能需要调整到通过Options手动创建实例 + return (TClient)clientCtr.Invoke(new object[] { cred, cloudCache.EndPoint, clientProfile }); + } + + throw new AbpException($"Failed to specify initialization Type for client {typeof(TClient).FullName}. Client instance could not be created"); + } +} diff --git a/aspnet-core/modules/common/LINGYUN.Abp.BlobStoring.Aliyun/LINGYUN/Abp/BlobStoring/Aliyun/AbpBlobStoringAliyunModule.cs b/aspnet-core/modules/common/LINGYUN.Abp.BlobStoring.Aliyun/LINGYUN/Abp/BlobStoring/Aliyun/AbpBlobStoringAliyunModule.cs index 3da8694e0..a8d591496 100644 --- a/aspnet-core/modules/common/LINGYUN.Abp.BlobStoring.Aliyun/LINGYUN/Abp/BlobStoring/Aliyun/AbpBlobStoringAliyunModule.cs +++ b/aspnet-core/modules/common/LINGYUN.Abp.BlobStoring.Aliyun/LINGYUN/Abp/BlobStoring/Aliyun/AbpBlobStoringAliyunModule.cs @@ -1,7 +1,4 @@ using LINGYUN.Abp.Aliyun; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using System.Collections.Generic; using Volo.Abp.BlobStoring; using Volo.Abp.Modularity; @@ -12,24 +9,25 @@ namespace LINGYUN.Abp.BlobStoring.Aliyun typeof(AbpAliyunModule))] public class AbpBlobStoringAliyunModule : AbpModule { - public override void ConfigureServices(ServiceConfigurationContext context) - { - var configuration = context.Services.GetConfiguration(); + // 需要时引用配置 + //public override void ConfigureServices(ServiceConfigurationContext context) + //{ + // var configuration = context.Services.GetConfiguration(); - Configure(options => - { - context.Services.ExecutePreConfiguredActions(options); - options.Containers.ConfigureAll((containerName, containerConfiguration) => - { - containerConfiguration.UseAliyun(aliyun => - { - aliyun.BucketName = configuration[AliyunBlobProviderConfigurationNames.BucketName] ?? ""; - aliyun.CreateBucketIfNotExists = configuration.GetSection(AliyunBlobProviderConfigurationNames.CreateBucketIfNotExists).Get(); - aliyun.CreateBucketReferer = configuration.GetSection(AliyunBlobProviderConfigurationNames.CreateBucketReferer).Get>(); - aliyun.Endpoint = configuration[AliyunBlobProviderConfigurationNames.Endpoint]; - }); - }); - }); - } + // Configure(options => + // { + // context.Services.ExecutePreConfiguredActions(options); + // options.Containers.ConfigureAll((containerName, containerConfiguration) => + // { + // containerConfiguration.UseAliyun(aliyun => + // { + // aliyun.BucketName = configuration[AliyunBlobProviderConfigurationNames.BucketName] ?? ""; + // aliyun.CreateBucketIfNotExists = configuration.GetSection(AliyunBlobProviderConfigurationNames.CreateBucketIfNotExists).Get(); + // aliyun.CreateBucketReferer = configuration.GetSection(AliyunBlobProviderConfigurationNames.CreateBucketReferer).Get>(); + // aliyun.Endpoint = configuration[AliyunBlobProviderConfigurationNames.Endpoint]; + // }); + // }); + // }); + //} } } diff --git a/aspnet-core/modules/common/LINGYUN.Abp.Sms.Aliyun/LINGYUN/Abp/Sms/Aliyun/AliyunSmsSender.cs b/aspnet-core/modules/common/LINGYUN.Abp.Sms.Aliyun/LINGYUN/Abp/Sms/Aliyun/AliyunSmsSender.cs index 34d1d9c90..c886b97e4 100644 --- a/aspnet-core/modules/common/LINGYUN.Abp.Sms.Aliyun/LINGYUN/Abp/Sms/Aliyun/AliyunSmsSender.cs +++ b/aspnet-core/modules/common/LINGYUN.Abp.Sms.Aliyun/LINGYUN/Abp/Sms/Aliyun/AliyunSmsSender.cs @@ -133,11 +133,17 @@ namespace LINGYUN.Abp.Sms.Aliyun private void TryAddTemplateParam(CommonRequest request, SmsMessage smsMessage) { - if (smsMessage.Properties.Count > 0) - { - var queryParamJson = JsonSerializer.Serialize(smsMessage.Properties); - request.AddQueryParameters("TemplateParam", queryParamJson); + // 统一一下模板参数名称 + if (smsMessage.Properties.TryGetValue("TemplateParam", out var templateParam)) + { + request.AddQueryParameters("TemplateParam", templateParam.ToString()); } + + //if (smsMessage.Properties.Count > 0) + //{ + // var queryParamJson = JsonSerializer.Serialize(smsMessage.Properties); + // request.AddQueryParameters("TemplateParam", queryParamJson); + //} } } } diff --git a/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Wrapper/Microsoft/AspNetCore/Cors/AbpCorsPolicyBuilderExtensions.cs b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Wrapper/Microsoft/AspNetCore/Cors/AbpCorsPolicyBuilderExtensions.cs new file mode 100644 index 000000000..e1f7b185c --- /dev/null +++ b/aspnet-core/modules/mvc/LINGYUN.Abp.AspNetCore.Mvc.Wrapper/Microsoft/AspNetCore/Cors/AbpCorsPolicyBuilderExtensions.cs @@ -0,0 +1,15 @@ +using LINGYUN.Abp.Wrapper; +using Microsoft.AspNetCore.Cors.Infrastructure; + +namespace Microsoft.AspNetCore.Cors; + +public static class AbpCorsPolicyBuilderExtensions +{ + public static CorsPolicyBuilder WithAbpWrapExposedHeaders(this CorsPolicyBuilder corsPolicyBuilder) + { + return corsPolicyBuilder + .WithExposedHeaders( + AbpHttpWrapConsts.AbpWrapResult, + AbpHttpWrapConsts.AbpDontWrapResult); + } +} diff --git a/aspnet-core/modules/oss-management/LINGYUN.Abp.OssManagement.Tencent/FodyWeavers.xml b/aspnet-core/modules/oss-management/LINGYUN.Abp.OssManagement.Tencent/FodyWeavers.xml new file mode 100644 index 000000000..1715698cc --- /dev/null +++ b/aspnet-core/modules/oss-management/LINGYUN.Abp.OssManagement.Tencent/FodyWeavers.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/aspnet-core/modules/oss-management/LINGYUN.Abp.OssManagement.Tencent/LINGYUN.Abp.OssManagement.Tencent.csproj b/aspnet-core/modules/oss-management/LINGYUN.Abp.OssManagement.Tencent/LINGYUN.Abp.OssManagement.Tencent.csproj new file mode 100644 index 000000000..d0518eb31 --- /dev/null +++ b/aspnet-core/modules/oss-management/LINGYUN.Abp.OssManagement.Tencent/LINGYUN.Abp.OssManagement.Tencent.csproj @@ -0,0 +1,16 @@ + + + + + + + netstandard2.0 + + + + + + + + + diff --git a/aspnet-core/modules/oss-management/LINGYUN.Abp.OssManagement.Tencent/LINGYUN/Abp/OssManagement/Tencent/AbpOssManagementTencentModule.cs b/aspnet-core/modules/oss-management/LINGYUN.Abp.OssManagement.Tencent/LINGYUN/Abp/OssManagement/Tencent/AbpOssManagementTencentModule.cs new file mode 100644 index 000000000..8c84947e2 --- /dev/null +++ b/aspnet-core/modules/oss-management/LINGYUN.Abp.OssManagement.Tencent/LINGYUN/Abp/OssManagement/Tencent/AbpOssManagementTencentModule.cs @@ -0,0 +1,17 @@ +using LINGYUN.Abp.BlobStoring.Tencent; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Modularity; + +namespace LINGYUN.Abp.OssManagement.Tencent +{ + [DependsOn( + typeof(AbpBlobStoringTencentCloudModule), + typeof(AbpOssManagementDomainModule))] + public class AbpOssManagementTencentModule : AbpModule + { + public override void ConfigureServices(ServiceConfigurationContext context) + { + context.Services.AddTransient(); + } + } +} diff --git a/aspnet-core/modules/oss-management/LINGYUN.Abp.OssManagement.Tencent/LINGYUN/Abp/OssManagement/Tencent/TencentOssContainer.cs b/aspnet-core/modules/oss-management/LINGYUN.Abp.OssManagement.Tencent/LINGYUN/Abp/OssManagement/Tencent/TencentOssContainer.cs new file mode 100644 index 000000000..aa1505ada --- /dev/null +++ b/aspnet-core/modules/oss-management/LINGYUN.Abp.OssManagement.Tencent/LINGYUN/Abp/OssManagement/Tencent/TencentOssContainer.cs @@ -0,0 +1,403 @@ +using COSXML; +using COSXML.Model.Bucket; +using COSXML.Model.Object; +using COSXML.Model.Service; +using LINGYUN.Abp.BlobStoring.Tencent; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Timing; + +namespace LINGYUN.Abp.OssManagement.Tencent +{ + /// + /// Oss容器的阿里云实现 + /// + internal class TencentOssContainer : IOssContainer + { + protected IClock Clock { get; } + protected ICurrentTenant CurrentTenant { get; } + protected ICosClientFactory CosClientFactory { get; } + public TencentOssContainer( + IClock clock, + ICurrentTenant currentTenant, + ICosClientFactory cosClientFactory) + { + Clock = clock; + CurrentTenant = currentTenant; + CosClientFactory = cosClientFactory; + } + public virtual async Task BulkDeleteObjectsAsync(BulkDeleteObjectRequest request) + { + var ossClient = await CreateClientAsync(); + + var path = GetBasePath(request.Path); + var deleteRequest = new DeleteMultiObjectRequest(request.Bucket); + deleteRequest.SetObjectKeys(request.Objects.Select(x => x += path).ToList()); + + ossClient.DeleteMultiObjects(deleteRequest); + } + + public virtual async Task CreateAsync(string name) + { + var ossClient = await CreateClientAsync(); + + if (BucketExists(ossClient, name)) + { + throw new BusinessException(code: OssManagementErrorCodes.ContainerAlreadyExists); + } + + var putBucketRequest = new PutBucketRequest(name); + var bucketResult = ossClient.PutBucket(putBucketRequest); + + return new OssContainer( + bucketResult.Key, + Clock.Now, + 0L, + Clock.Now, + new Dictionary + { + { "Id", bucketResult.Key }, + { "DisplayName", bucketResult.Key } + }); + } + + public virtual async Task CreateObjectAsync(CreateOssObjectRequest request) + { + var ossClient = await CreateClientAsync(); + + var objectPath = GetBasePath(request.Path); + + var objectName = objectPath.IsNullOrWhiteSpace() + ? request.Object + : objectPath + request.Object; + + if (!request.Overwrite && ObjectExists(ossClient, request.Bucket, objectName)) + { + throw new BusinessException(code: OssManagementErrorCodes.ObjectAlreadyExists); + } + + // 当一个对象名称是以 / 结尾时,不论该对象是否存有数据,都以目录的形式存在 + // 详情见:https://help.aliyun.com/document_detail/31910.html + if (objectName.EndsWith("/") && + request.Content.IsNullOrEmpty()) + { + var emptyStream = new MemoryStream(); + var emptyData = System.Text.Encoding.UTF8.GetBytes(""); + await emptyStream.WriteAsync(emptyData, 0, emptyData.Length); + request.SetContent(emptyStream); + } + + // 没有bucket则创建 + if (!BucketExists(ossClient, request.Bucket)) + { + var putBucketRequest = new PutBucketRequest(request.Bucket); + ossClient.PutBucket(putBucketRequest); + } + + var contentLength = request.Content.Length; + var putObjectRequest = new PutObjectRequest(request.Bucket, objectName, request.Content); + + var objectResult = ossClient.PutObject(putObjectRequest); + + if (objectResult.IsSuccessful() && request.ExpirationTime.HasValue) + { + var putBuckerLifeRequest = new PutBucketLifecycleRequest(request.Bucket); + + var rule = new COSXML.Model.Tag.LifecycleConfiguration.Rule(); + rule.id = "lfiecycleConfigureId"; + rule.status = "Enabled"; //Enabled,Disabled + + rule.filter = new COSXML.Model.Tag.LifecycleConfiguration.Filter(); + // TODO: 需要测试 + rule.filter.prefix = objectName; + + putBuckerLifeRequest.SetRule(rule); + + ossClient.PutBucketLifecycle(putBuckerLifeRequest); + } + + var ossObject = new OssObject( + !objectPath.IsNullOrWhiteSpace() + ? objectName.Replace(objectPath, "") + : objectName, + objectPath, + objectResult.eTag, + DateTime.Now, + contentLength, + DateTime.Now, + new Dictionary(), + objectName.EndsWith("/") // 名称结尾是 / 符号的则为目录:https://cloud.tencent.com/document/product/436/13324 + ) + { + FullName = objectName + }; + + if (!Equals(request.Content, Stream.Null)) + { + request.Content.Seek(0, SeekOrigin.Begin); + ossObject.SetContent(request.Content); + } + + return ossObject; + } + + public virtual async Task DeleteAsync(string name) + { + // 阿里云oss在控制台设置即可,无需改变 + var ossClient = await CreateClientAsync(); + + if (BucketExists(ossClient, name)) + { + var deleteBucketRequest = new DeleteBucketRequest(name); + ossClient.DeleteBucket(deleteBucketRequest); + } + } + + public virtual async Task DeleteObjectAsync(GetOssObjectRequest request) + { + var ossClient = await CreateClientAsync(); + + var objectPath = GetBasePath(request.Path); + + var objectName = objectPath.IsNullOrWhiteSpace() + ? request.Object + : objectPath + request.Object; + + if (BucketExists(ossClient, request.Bucket) && + ObjectExists(ossClient, request.Bucket, objectName)) + { + var getBucketRequest = new GetBucketRequest(request.Bucket); + + var getBucketResult = ossClient.GetBucket(getBucketRequest); + if (getBucketResult.listBucket.commonPrefixesList.Any() || + getBucketResult.listBucket.contentsList.Any()) + { + throw new BusinessException(code: OssManagementErrorCodes.ObjectDeleteWithNotEmpty); + } + var deleteObjectRequest = new DeleteObjectRequest(request.Bucket, objectName); + ossClient.DeleteObject(deleteObjectRequest); + } + } + + public virtual async Task ExistsAsync(string name) + { + var ossClient = await CreateClientAsync(); + + return BucketExists(ossClient, name); + } + + public virtual async Task GetAsync(string name) + { + var ossClient = await CreateClientAsync(); + if (!BucketExists(ossClient, name)) + { + throw new BusinessException(code: OssManagementErrorCodes.ContainerNotFound); + // throw new ContainerNotFoundException($"Can't not found container {name} in aliyun blob storing"); + } + var getBucketRequest = new GetBucketRequest(name); + var bucket = ossClient.GetBucket(getBucketRequest); + + return new OssContainer( + bucket.Key, + new DateTime(1970, 1, 1, 0, 0, 0), // TODO: 从header获取? 需要测试 + 0L, + null, + new Dictionary + { + { "Id", bucket.Key }, + { "DisplayName", bucket.Key } + }); + } + + public virtual async Task GetObjectAsync(GetOssObjectRequest request) + { + var ossClient = await CreateClientAsync(); + if (!BucketExists(ossClient, request.Bucket)) + { + throw new BusinessException(code: OssManagementErrorCodes.ContainerNotFound); + // throw new ContainerNotFoundException($"Can't not found container {request.Bucket} in aliyun blob storing"); + } + + var objectPath = GetBasePath(request.Path); + var objectName = objectPath.IsNullOrWhiteSpace() + ? request.Object + : objectPath + request.Object; + + if (!ObjectExists(ossClient, request.Bucket, objectName)) + { + throw new BusinessException(code: OssManagementErrorCodes.ObjectNotFound); + // throw new ContainerNotFoundException($"Can't not found object {objectName} in container {request.Bucket} with aliyun blob storing"); + } + + var getObjectRequest = new GetObjectBytesRequest(request.Bucket, objectName); + if (!request.Process.IsNullOrWhiteSpace()) + { + getObjectRequest.SetQueryParameter(request.Process, null); + } + var objectResult = ossClient.GetObject(getObjectRequest); + var ossObject = new OssObject( + !objectPath.IsNullOrWhiteSpace() + ? objectResult.Key.Replace(objectPath, "") + : objectResult.Key, + request.Path, + objectResult.eTag, + null, + objectResult.content.Length, + null, + new Dictionary(), + objectResult.Key.EndsWith("/")) + { + FullName = objectResult.Key + }; + + if (objectResult.content.Length > 0) + { + var memoryStream = new MemoryStream(); + await memoryStream.WriteAsync(objectResult.content, 0, objectResult.content.Length); + memoryStream.Seek(0, SeekOrigin.Begin); + ossObject.SetContent(memoryStream); + } + + return ossObject; + } + + public virtual async Task GetListAsync(GetOssContainersRequest request) + { + var ossClient = await CreateClientAsync(); + + // TODO: 腾讯云直接返回所有列表? + var getBucketRequest = new GetServiceRequest(); + var bucket = ossClient.GetService(getBucketRequest); + + return new GetOssContainersResponse( + request.Prefix, + request.Marker, + null, + bucket.listAllMyBuckets.buckets.Count, + bucket.listAllMyBuckets.buckets + .Select(x => new OssContainer( + x.name, + DateTime.TryParse(x.createDate, out var time) ? time : new DateTime(1970, 1, 1), + 0L, + null, + new Dictionary + { + { "Id", x.name }, + { "DisplayName", x.name } + })) + .ToList()); + } + + public virtual async Task GetObjectsAsync(GetOssObjectsRequest request) + { + + var ossClient = await CreateClientAsync(); + + var objectPath = GetBasePath(request.Prefix); + var marker = !objectPath.IsNullOrWhiteSpace() && !request.Marker.IsNullOrWhiteSpace() + ? request.Marker.Replace(objectPath, "") + : request.Marker; + + // TODO: 阿里云的分页差异需要前端来弥补,传递Marker, 按照Oss控制台的逻辑,直接把MaxKeys设置较大值就行了 + + var getBucketRequest = new GetBucketRequest(request.BucketName); + getBucketRequest.SetMarker(!marker.IsNullOrWhiteSpace() ? objectPath + marker : marker); + getBucketRequest.SetMaxKeys(request.MaxKeys?.ToString() ?? "10"); + getBucketRequest.SetPrefix(objectPath); + getBucketRequest.SetDelimiter(request.Delimiter); + getBucketRequest.SetEncodingType(request.EncodingType); + + var getBucketResult = ossClient.GetBucket(getBucketRequest); + + var ossObjects = getBucketResult.listBucket.contentsList + .Where(x => !x.key.Equals(objectPath))// 过滤当前的目录返回值 + .Select(x => new OssObject( + !objectPath.IsNullOrWhiteSpace() && !x.key.Equals(objectPath) + ? x.key.Replace(objectPath, "") + : x.key, // 去除目录名称 + request.Prefix, + x.eTag, + DateTime.TryParse(x.lastModified, out var ctime) ? ctime : null, + x.size, + DateTime.TryParse(x.lastModified, out var mtime) ? mtime : null, + new Dictionary + { + { "Id", x.key }, + { "DisplayName", x.key } + }, + x.key.EndsWith("/")) + { + FullName = x.key + }) + .ToList(); + // 当 Delimiter 为 / 时, objectsResponse.CommonPrefixes 可用于代表层级目录 + if (getBucketResult.listBucket.commonPrefixesList.Any()) + { + ossObjects.InsertRange(0, + getBucketResult.listBucket.commonPrefixesList + .Select(x => new OssObject( + x.prefix.Replace(objectPath, ""), + request.Prefix, + "", + null, + 0L, + null, + null, + true))); + } + // 排序 + // TODO: 是否需要客户端来排序 + ossObjects.Sort(new OssObjectComparer()); + + return new GetOssObjectsResponse( + getBucketResult.Key, + request.Prefix, + marker, + !objectPath.IsNullOrWhiteSpace() && !getBucketResult.listBucket.nextMarker.IsNullOrWhiteSpace() + ? getBucketResult.listBucket.nextMarker.Replace(objectPath, "") + : getBucketResult.listBucket.nextMarker, + getBucketResult.listBucket.delimiter, + getBucketResult.listBucket.maxKeys, + ossObjects); + } + + protected virtual string GetBasePath(string path) + { + string objectPath = ""; + if (CurrentTenant.Id == null) + { + objectPath += "host/"; + } + else + { + objectPath += "tenants/" + CurrentTenant.Id.Value.ToString("D"); + } + + objectPath += path ?? ""; + + return objectPath.EnsureEndsWith('/'); + } + + protected virtual bool BucketExists(CosXml cos, string bucketName) + { + var request = new DoesBucketExistRequest(bucketName); + return cos.DoesBucketExist(request); + } + + protected virtual bool ObjectExists(CosXml cos, string bucketName, string objectName) + { + var request = new DoesObjectExistRequest(bucketName, objectName); + return cos.DoesObjectExist(request); + } + + protected virtual async Task CreateClientAsync() + { + return await CosClientFactory.CreateAsync(); + } + } +} diff --git a/aspnet-core/modules/oss-management/LINGYUN.Abp.OssManagement.Tencent/LINGYUN/Abp/OssManagement/Tencent/TencentOssContainerFactory.cs b/aspnet-core/modules/oss-management/LINGYUN.Abp.OssManagement.Tencent/LINGYUN/Abp/OssManagement/Tencent/TencentOssContainerFactory.cs new file mode 100644 index 000000000..5c64675f0 --- /dev/null +++ b/aspnet-core/modules/oss-management/LINGYUN.Abp.OssManagement.Tencent/LINGYUN/Abp/OssManagement/Tencent/TencentOssContainerFactory.cs @@ -0,0 +1,31 @@ +using LINGYUN.Abp.BlobStoring.Tencent; +using Volo.Abp.MultiTenancy; +using Volo.Abp.Timing; + +namespace LINGYUN.Abp.OssManagement.Tencent +{ + public class TencentOssContainerFactory : IOssContainerFactory + { + protected IClock Clock { get; } + protected ICurrentTenant CurrentTenant { get; } + protected ICosClientFactory CosClientFactory { get; } + + public TencentOssContainerFactory( + IClock clock, + ICurrentTenant currentTenant, + ICosClientFactory cosClientFactory) + { + Clock = clock; + CurrentTenant = currentTenant; + CosClientFactory = cosClientFactory; + } + + public IOssContainer Create() + { + return new TencentOssContainer( + Clock, + CurrentTenant, + CosClientFactory); + } + } +} diff --git a/aspnet-core/modules/oss-management/LINGYUN.Abp.OssManagement.Tencent/README.md b/aspnet-core/modules/oss-management/LINGYUN.Abp.OssManagement.Tencent/README.md new file mode 100644 index 000000000..e46d7a4ba --- /dev/null +++ b/aspnet-core/modules/oss-management/LINGYUN.Abp.OssManagement.Tencent/README.md @@ -0,0 +1,15 @@ +# LINGYUN.Abp.OssManagement.Tencent + +腾讯云oss容器接口 + +## 配置使用 + +模块按需引用 + +```csharp +[DependsOn(typeof(AbpOssManagementTencentCloudModule))] +public class YouProjectModule : AbpModule +{ + // other +} +```