49 changed files with 2191 additions and 42 deletions
@ -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 |
|||
@ -0,0 +1,21 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>netstandard2.0</TargetFramework> |
|||
<RootNamespace /> |
|||
<Description>腾讯云Oss对象存储Abp集成</Description> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.BlobStoring" Version="$(VoloAbpPackageVersion)" /> |
|||
<PackageReference Include="Tencent.QCloud.Cos.Sdk" Version="5.4.*" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\LINGYUN.Abp.Tencent\LINGYUN.Abp.Tencent.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -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 |
|||
{ |
|||
} |
|||
@ -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<CosXml, TencentBlobProviderConfiguration>, |
|||
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<CosXml> CreateAsync<TContainer>() |
|||
{ |
|||
var configuration = ConfigurationProvider.Get<TContainer>(); |
|||
|
|||
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); |
|||
} |
|||
} |
|||
@ -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}"; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,11 @@ |
|||
using COSXML; |
|||
using System.Threading.Tasks; |
|||
|
|||
namespace LINGYUN.Abp.BlobStoring.Tencent; |
|||
|
|||
public interface ICosClientFactory |
|||
{ |
|||
Task<CosXml> CreateAsync<TContainer>(); |
|||
|
|||
Task<CosXml> CreateAsync(TencentBlobProviderConfiguration configuration); |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
using Volo.Abp.BlobStoring; |
|||
|
|||
namespace LINGYUN.Abp.BlobStoring.Tencent |
|||
{ |
|||
public interface ITencentBlobNameCalculator |
|||
{ |
|||
string Calculate(BlobProviderArgs args); |
|||
} |
|||
} |
|||
@ -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<TencentBlobProviderConfiguration> aliyunConfigureAction) |
|||
{ |
|||
containerConfiguration.ProviderType = typeof(TencentCloudBlobProvider); |
|||
|
|||
aliyunConfigureAction(new TencentBlobProviderConfiguration(containerConfiguration)); |
|||
|
|||
return containerConfiguration; |
|||
} |
|||
} |
|||
} |
|||
@ -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 |
|||
{ |
|||
/// <summary>
|
|||
/// 腾讯云对象命名规范
|
|||
/// https://cloud.tencent.com/document/product/436/13324
|
|||
/// </summary>
|
|||
/// <param name="blobName"></param>
|
|||
/// <returns></returns>
|
|||
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; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 腾讯云BucketName命名规范
|
|||
/// https://cloud.tencent.com/document/product/436/13312
|
|||
/// </summary>
|
|||
/// <param name="containerName"></param>
|
|||
/// <returns></returns>
|
|||
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; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,63 @@ |
|||
using System.Collections.Generic; |
|||
using Volo.Abp; |
|||
using Volo.Abp.BlobStoring; |
|||
|
|||
namespace LINGYUN.Abp.BlobStoring.Tencent |
|||
{ |
|||
public class TencentBlobProviderConfiguration |
|||
{ |
|||
/// <summary>
|
|||
/// AppId
|
|||
/// </summary>
|
|||
public string AppId { |
|||
get => _containerConfiguration.GetConfiguration<string>(TencentBlobProviderConfigurationNames.AppId); |
|||
set => _containerConfiguration.SetConfiguration(TencentBlobProviderConfigurationNames.AppId, Check.NotNullOrWhiteSpace(value, nameof(value))); |
|||
} |
|||
/// <summary>
|
|||
/// 区域
|
|||
/// </summary>
|
|||
public string Region { |
|||
get => _containerConfiguration.GetConfiguration<string>(TencentBlobProviderConfigurationNames.Region); |
|||
set => _containerConfiguration.SetConfiguration(TencentBlobProviderConfigurationNames.Region, value); |
|||
} |
|||
/// <summary>
|
|||
/// 命名空间
|
|||
/// </summary>
|
|||
public string BucketName |
|||
{ |
|||
get => _containerConfiguration.GetConfiguration<string>(TencentBlobProviderConfigurationNames.BucketName); |
|||
set => _containerConfiguration.SetConfiguration(TencentBlobProviderConfigurationNames.BucketName, Check.NotNullOrWhiteSpace(value, nameof(value))); |
|||
} |
|||
/// <summary>
|
|||
/// 命名空间不存在是否创建
|
|||
/// </summary>
|
|||
public bool CreateBucketIfNotExists |
|||
{ |
|||
get => _containerConfiguration.GetConfigurationOrDefault(TencentBlobProviderConfigurationNames.CreateBucketIfNotExists, false); |
|||
set => _containerConfiguration.SetConfiguration(TencentBlobProviderConfigurationNames.CreateBucketIfNotExists, value); |
|||
} |
|||
/// <summary>
|
|||
/// 创建命名空间时防盗链列表
|
|||
/// </summary>
|
|||
public List<string> CreateBucketReferer { |
|||
get => _containerConfiguration.GetConfiguration<List<string>>(TencentBlobProviderConfigurationNames.CreateBucketReferer); |
|||
set { |
|||
if (value == null) |
|||
{ |
|||
_containerConfiguration.SetConfiguration(TencentBlobProviderConfigurationNames.CreateBucketReferer, new List<string>()); |
|||
} |
|||
else |
|||
{ |
|||
_containerConfiguration.SetConfiguration(TencentBlobProviderConfigurationNames.CreateBucketReferer, value); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private readonly BlobContainerConfiguration _containerConfiguration; |
|||
|
|||
public TencentBlobProviderConfiguration(BlobContainerConfiguration containerConfiguration) |
|||
{ |
|||
_containerConfiguration = containerConfiguration; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
namespace LINGYUN.Abp.BlobStoring.Tencent |
|||
{ |
|||
public static class TencentBlobProviderConfigurationNames |
|||
{ |
|||
/// <summary>
|
|||
/// AppId
|
|||
/// </summary>
|
|||
public const string AppId = "Tencent:OSS:AppId"; |
|||
/// <summary>
|
|||
/// 区域
|
|||
/// </summary>
|
|||
public const string Region = "Tencent:OSS:Region"; |
|||
/// <summary>
|
|||
/// 命名空间
|
|||
/// </summary>
|
|||
public const string BucketName = "Tencent:OSS:BucketName"; |
|||
/// <summary>
|
|||
/// 命名空间不存在是否创建
|
|||
/// </summary>
|
|||
public const string CreateBucketIfNotExists = "Tencent:OSS:CreateBucketIfNotExists"; |
|||
/// <summary>
|
|||
/// 创建命名空间时防盗链列表
|
|||
/// </summary>
|
|||
public const string CreateBucketReferer = "Tencent:OSS:CreateBucketReferer"; |
|||
} |
|||
} |
|||
@ -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<bool> 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<bool> ExistsAsync(BlobProviderExistsArgs args) |
|||
{ |
|||
var ossClient = await GetOssClientAsync(args); |
|||
var blobName = TencentBlobNameCalculator.Calculate(args); |
|||
|
|||
return await BlobExistsAsync(ossClient, args, blobName); |
|||
} |
|||
|
|||
public override async Task<Stream> 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<CosXml> 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<string> 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<bool> 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<bool> 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; |
|||
} |
|||
} |
|||
@ -0,0 +1,23 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>netstandard2.0</TargetFramework> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<None Remove="LINGYUN\Abp\Sms\Tencent\Localization\Resources\*.json" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<EmbeddedResource Include="LINGYUN\Abp\Sms\Tencent\Localization\Resources\*.json" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.Sms" Version="$(VoloAbpPackageVersion)" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\LINGYUN.Abp.Tencent\LINGYUN.Abp.Tencent.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -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<AbpVirtualFileSystemOptions>(options => |
|||
{ |
|||
options.FileSets.AddEmbedded<AbpSmsTencentModule>(); |
|||
}); |
|||
|
|||
Configure<AbpLocalizationOptions>(options => |
|||
{ |
|||
options.Resources |
|||
.Get<TencentCloudResource>() |
|||
.AddVirtualJson("/LINGYUN/Abp/Sms/Tencent/Localization/Resources"); |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -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." |
|||
} |
|||
} |
|||
@ -0,0 +1,12 @@ |
|||
{ |
|||
"culture": "zh-Hans", |
|||
"texts": { |
|||
"DisplayName:TenantCloud.SmsSetting": "短信设置", |
|||
"DisplayName:AppId": "应用Id", |
|||
"Description:AppId": "在腾讯云短信控制平台申请的应用标识", |
|||
"DisplayName:DefaultSignName": "默认签名", |
|||
"Description:DefaultSignName": "当短信消息体未指定签名时的默认签名", |
|||
"DisplayName:DefaultTemplateId": "默认模板", |
|||
"Description:DefaultTemplateId": "当短信消息体未指定模板号时的默认模板标识" |
|||
} |
|||
} |
|||
@ -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"; |
|||
|
|||
/// <summary>
|
|||
/// 短信 SdkAppId
|
|||
/// 在 短信控制台 添加应用后生成的实际 SdkAppId,示例如1400006666。
|
|||
/// </summary>
|
|||
public const string AppId = Prefix + ".Domain"; |
|||
/// <summary>
|
|||
/// 短信签名内容
|
|||
/// </summary>
|
|||
public const string DefaultSignName = Prefix + ".DefaultSignName"; |
|||
/// <summary>
|
|||
/// 默认短信模板 ID
|
|||
/// </summary>
|
|||
public const string DefaultTemplateId = Prefix + ".DefaultTemplateId"; |
|||
} |
|||
} |
|||
@ -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<TencentCloudResource>(name); |
|||
} |
|||
} |
|||
} |
|||
@ -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<TencentCloudSmsSender> Logger { protected get; set; } |
|||
|
|||
protected IJsonSerializer JsonSerializer { get; } |
|||
protected ISettingProvider SettingProvider { get; } |
|||
protected IServiceProvider ServiceProvider { get; } |
|||
protected TencentCloudClientFactory<SmsClient> TencentCloudClientFactory { get; } |
|||
public TencentCloudSmsSender( |
|||
IJsonSerializer jsonSerializer, |
|||
ISettingProvider settingProvider, |
|||
IServiceProvider serviceProvider, |
|||
TencentCloudClientFactory<SmsClient> tencentCloudClientFactory) |
|||
{ |
|||
JsonSerializer = jsonSerializer; |
|||
SettingProvider = settingProvider; |
|||
ServiceProvider = serviceProvider; |
|||
TencentCloudClientFactory = tencentCloudClientFactory; |
|||
|
|||
Logger = NullLogger<TencentCloudSmsSender>.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)); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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 |
|||
{ |
|||
/// <summary>
|
|||
/// 扩展短信接口
|
|||
/// </summary>
|
|||
/// <param name="smsSender"></param>
|
|||
/// <param name="templateCode">短信模板号</param>
|
|||
/// <param name="phoneNumber">发送手机号</param>
|
|||
/// <param name="templateParams">短信模板参数</param>
|
|||
/// <returns></returns>
|
|||
public static async Task SendAsync( |
|||
this ISmsSender smsSender, |
|||
string templateCode, |
|||
string phoneNumber, |
|||
IDictionary<string, object> 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); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// 扩展短信接口
|
|||
/// </summary>
|
|||
/// <param name="smsSender"></param>
|
|||
/// <param name="signName">短信签名</param>
|
|||
/// <param name="templateCode">短信模板号</param>
|
|||
/// <param name="phoneNumber">发送手机号</param>
|
|||
/// <param name="templateParams">短信模板参数</param>
|
|||
/// <returns></returns>
|
|||
public static async Task SendAsync( |
|||
this ISmsSender smsSender, |
|||
string signName, |
|||
string templateCode, |
|||
string phoneNumber, |
|||
IDictionary<string, object> 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); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>net6.0</TargetFramework> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<EmbeddedResource Include="LINGYUN\Abp\Aliyun\SettingManagement\Localization\Resources\*.json" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<PackageReference Include="Volo.Abp.AspNetCore.Mvc" Version="$(VoloAbpPackageVersion)" /> |
|||
<PackageReference Include="Volo.Abp.SettingManagement.Domain" Version="$(VoloAbpPackageVersion)" /> |
|||
</ItemGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\settings\LINGYUN.Abp.SettingManagement.Application.Contracts\LINGYUN.Abp.SettingManagement.Application.Contracts.csproj" /> |
|||
<ProjectReference Include="..\LINGYUN.Abp.Sms.Tencent\LINGYUN.Abp.Sms.Tencent.csproj" /> |
|||
<ProjectReference Include="..\LINGYUN.Abp.Tencent\LINGYUN.Abp.Tencent.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -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 |
|||
{ |
|||
|
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
using LINGYUN.Abp.SettingManagement; |
|||
|
|||
namespace LINGYUN.Abp.Tencent.SettingManagement; |
|||
|
|||
public interface ITenantCloudSettingAppService : IReadonlySettingAppService |
|||
{ |
|||
} |
|||
@ -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<SettingGroupResult> GetAllForCurrentTenantAsync() |
|||
{ |
|||
return await GetAllForProviderAsync(TenantSettingValueProvider.ProviderName, CurrentTenant.GetId().ToString()); |
|||
} |
|||
|
|||
public virtual async Task<SettingGroupResult> GetAllForGlobalAsync() |
|||
{ |
|||
return await GetAllForProviderAsync(GlobalSettingValueProvider.ProviderName, null); |
|||
} |
|||
|
|||
protected virtual async Task<SettingGroupResult> 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<OptionDto> 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"), |
|||
}; |
|||
} |
|||
} |
|||
@ -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<TencentCloudResource>(name); |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
namespace LINGYUN.Abp.Tencent.SettingManagement |
|||
{ |
|||
public class TenantCloudSettingPermissionNames |
|||
{ |
|||
public const string GroupName = "Abp.Tencent"; |
|||
|
|||
public const string Settings = GroupName + ".Settings"; |
|||
} |
|||
} |
|||
@ -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<Type, Func<Credential, string, ClientProfile, AbstractClient>> ClientProxies { get; } |
|||
|
|||
public AbpTencentCloudOptions() |
|||
{ |
|||
ClientProxies = new Dictionary<Type, Func<Credential, string, ClientProfile, AbstractClient>>(); |
|||
} |
|||
} |
|||
@ -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<TClient> |
|||
{ |
|||
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<TClient> CreateAsync() |
|||
{ |
|||
var clientCacheItem = await GetClientCacheItemAsync(); |
|||
|
|||
return CreateClient(clientCacheItem); |
|||
} |
|||
|
|||
protected abstract TClient CreateClient(TencentCloudClientCacheItem cloudCache); |
|||
|
|||
protected virtual async Task<TencentCloudClientCacheItem> 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<TClient, TConfiguration> |
|||
{ |
|||
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<TClient> CreateAsync(TConfiguration configuration) |
|||
{ |
|||
var clientCacheItem = await GetClientCacheItemAsync(); |
|||
|
|||
return CreateClient(configuration, clientCacheItem); |
|||
} |
|||
|
|||
protected abstract TClient CreateClient(TConfiguration configuration, TencentCloudClientCacheItem cloudCache); |
|||
|
|||
protected virtual async Task<TencentCloudClientCacheItem> 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, |
|||
}; |
|||
}); |
|||
} |
|||
} |
|||
@ -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)" |
|||
} |
|||
} |
|||
@ -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": "北美地区(多伦多)" |
|||
} |
|||
} |
|||
@ -0,0 +1,9 @@ |
|||
using Volo.Abp.Localization; |
|||
|
|||
namespace LINGYUN.Abp.Tencent.Localization |
|||
{ |
|||
[LocalizationResourceName("TencentCloud")] |
|||
public class TencentCloudResource |
|||
{ |
|||
} |
|||
} |
|||
@ -1,9 +0,0 @@ |
|||
using Volo.Abp.Localization; |
|||
|
|||
namespace LINYUN.Abp.Tencent.Localization |
|||
{ |
|||
[LocalizationResourceName("Tencent")] |
|||
public class TencentResource |
|||
{ |
|||
} |
|||
} |
|||
@ -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<TencentCloudResource>(name); |
|||
} |
|||
} |
|||
@ -0,0 +1,47 @@ |
|||
namespace LINGYUN.Abp.Tencent.Settings; |
|||
|
|||
public static class TencentCloudSettingNames |
|||
{ |
|||
public const string Prefix = "Abp.TencentCloud"; |
|||
/// <summary>
|
|||
/// SecretId
|
|||
/// </summary>
|
|||
public const string SecretId = Prefix + ".SecretId"; |
|||
/// <summary>
|
|||
/// SecretKey
|
|||
/// </summary>
|
|||
public const string SecretKey = Prefix + ".SecretKey"; |
|||
/// <summary>
|
|||
/// 连接地域
|
|||
/// </summary>
|
|||
public const string EndPoint = Prefix + ".EndPoint"; |
|||
/// <summary>
|
|||
/// 会话持续时间
|
|||
/// </summary>
|
|||
public const string DurationSecond = Prefix + ".DurationSecond"; |
|||
/// <summary>
|
|||
/// 连接设置
|
|||
/// </summary>
|
|||
public class Connection |
|||
{ |
|||
public const string Prefix = TencentCloudSettingNames.Prefix + ".Connection"; |
|||
/// <summary>
|
|||
/// 代理服务器
|
|||
/// </summary>
|
|||
public const string WebProxy = Prefix + ".WebProxy"; |
|||
/// <summary>
|
|||
/// 连接地域
|
|||
/// 按照腾讯云文档,对于金融区服务需要指定域名
|
|||
/// </summary>
|
|||
public const string EndPoint = Prefix + ".EndPoint"; |
|||
/// <summary>
|
|||
/// Http方法,按照腾讯云文档,不同的方法对于请求大小有限制
|
|||
/// 默认:POST
|
|||
/// </summary>
|
|||
public const string HttpMethod = Prefix + ".HttpMethod"; |
|||
/// <summary>
|
|||
/// 超时时间
|
|||
/// </summary>
|
|||
public const string Timeout = Prefix + ".Timeout"; |
|||
} |
|||
} |
|||
@ -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"); |
|||
} |
|||
} |
|||
@ -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<TClient> : AbstractTencentCloudClientFactory<TClient> |
|||
{ |
|||
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"); |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd"> |
|||
<ConfigureAwait ContinueOnCapturedContext="false" /> |
|||
</Weavers> |
|||
@ -0,0 +1,16 @@ |
|||
<Project Sdk="Microsoft.NET.Sdk"> |
|||
|
|||
<Import Project="..\..\..\configureawait.props" /> |
|||
<Import Project="..\..\..\common.props" /> |
|||
|
|||
<PropertyGroup> |
|||
<TargetFramework>netstandard2.0</TargetFramework> |
|||
<RootNamespace /> |
|||
</PropertyGroup> |
|||
|
|||
<ItemGroup> |
|||
<ProjectReference Include="..\..\cloud-tencent\LINGYUN.Abp.BlobStoring.Tencent\LINGYUN.Abp.BlobStoring.Tencent.csproj" /> |
|||
<ProjectReference Include="..\LINGYUN.Abp.OssManagement.Domain\LINGYUN.Abp.OssManagement.Domain.csproj" /> |
|||
</ItemGroup> |
|||
|
|||
</Project> |
|||
@ -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<IOssContainerFactory, TencentOssContainerFactory>(); |
|||
} |
|||
} |
|||
} |
|||
@ -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 |
|||
{ |
|||
/// <summary>
|
|||
/// Oss容器的阿里云实现
|
|||
/// </summary>
|
|||
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<OssContainer> 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<string, string> |
|||
{ |
|||
{ "Id", bucketResult.Key }, |
|||
{ "DisplayName", bucketResult.Key } |
|||
}); |
|||
} |
|||
|
|||
public virtual async Task<OssObject> 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<string, string>(), |
|||
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<bool> ExistsAsync(string name) |
|||
{ |
|||
var ossClient = await CreateClientAsync(); |
|||
|
|||
return BucketExists(ossClient, name); |
|||
} |
|||
|
|||
public virtual async Task<OssContainer> 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<string, string> |
|||
{ |
|||
{ "Id", bucket.Key }, |
|||
{ "DisplayName", bucket.Key } |
|||
}); |
|||
} |
|||
|
|||
public virtual async Task<OssObject> 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<string, string>(), |
|||
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<GetOssContainersResponse> 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<string, string> |
|||
{ |
|||
{ "Id", x.name }, |
|||
{ "DisplayName", x.name } |
|||
})) |
|||
.ToList()); |
|||
} |
|||
|
|||
public virtual async Task<GetOssObjectsResponse> 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<string, string> |
|||
{ |
|||
{ "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<CosXml> CreateClientAsync() |
|||
{ |
|||
return await CosClientFactory.CreateAsync<AbpOssManagementContainer>(); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,15 @@ |
|||
# LINGYUN.Abp.OssManagement.Tencent |
|||
|
|||
腾讯云oss容器接口 |
|||
|
|||
## 配置使用 |
|||
|
|||
模块按需引用 |
|||
|
|||
```csharp |
|||
[DependsOn(typeof(AbpOssManagementTencentCloudModule))] |
|||
public class YouProjectModule : AbpModule |
|||
{ |
|||
// other |
|||
} |
|||
``` |
|||
Loading…
Reference in new issue