From 2c3a8842642bb9c37bc551c2b4351754be8958cb Mon Sep 17 00:00:00 2001 From: xiaolipro <2357729423@qq.com> Date: Wed, 16 Sep 2026 15:47:39 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AF=B9=E6=8E=A5=E8=85=BE=E8=AE=AF?= =?UTF-8?q?=E4=BA=91cos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Directory.Build.props | 2 +- .../appsettings.Development.json | 15 + .../ForwardCore/DefaultForwardFlow.cs | 49 +++- .../InterfaceForward.Application.csproj | 10 +- .../InterfaceForwardApplicationModule.cs | 2 + .../OSS/OSSHelper.cs | 263 ------------------ .../OSS/OSSOptions.cs | 32 --- .../OSS/UploadResultDto.cs | 30 -- .../Services/App/AppsService.cs | 36 ++- 9 files changed, 88 insertions(+), 351 deletions(-) delete mode 100644 src/InterfaceForward.Application/OSS/OSSHelper.cs delete mode 100644 src/InterfaceForward.Application/OSS/OSSOptions.cs delete mode 100644 src/InterfaceForward.Application/OSS/UploadResultDto.cs diff --git a/Directory.Build.props b/Directory.Build.props index e7da47d..7d6f04d 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,6 +1,6 @@ - 8.0.5.16 + 8.0.5.17 diff --git a/src/InterfaceForward.Api/appsettings.Development.json b/src/InterfaceForward.Api/appsettings.Development.json index a6e86ac..92b8138 100644 --- a/src/InterfaceForward.Api/appsettings.Development.json +++ b/src/InterfaceForward.Api/appsettings.Development.json @@ -4,5 +4,20 @@ "Default": "Debug", "Microsoft.AspNetCore": "Warning" } + }, + "ObjectStorage": { + "Provider": "TencentCos", + "SignUrlsByDefault": true, + "DefaultSignedUrlExpiresSeconds": 900, + "DefaultUploadExpiresSeconds": 600, + "TencentCos": { + "Region": "ap-guangzhou", + "SecretId": "替换为腾讯云SecretId", + "SecretKey": "替换为腾讯云SecretKey", + "Bucket": "your-bucket-1250000000", + "UseHttps": true, + "SignUrls": true, + "DurationSecond": 600 + } } } diff --git a/src/InterfaceForward.Application/ForwardCore/DefaultForwardFlow.cs b/src/InterfaceForward.Application/ForwardCore/DefaultForwardFlow.cs index 83ef74a..2e148c9 100644 --- a/src/InterfaceForward.Application/ForwardCore/DefaultForwardFlow.cs +++ b/src/InterfaceForward.Application/ForwardCore/DefaultForwardFlow.cs @@ -5,9 +5,9 @@ using System.Text.RegularExpressions; using System.Web; using System.Xml; using Fake.DependencyInjection; +using Fake.ObjectStorage; using InterfaceForward.Application.Contracts.ForwardCore; using InterfaceForward.Application.Helpers; -using InterfaceForward.Application.OSS; using InterfaceForward.Domain.Shared; using InterfaceForward.Domain.Shared.Enum; using Microsoft.AspNetCore.Components; @@ -23,6 +23,7 @@ public class DefaultForwardFlow : IForwardFlow, ITransientDependency { private static readonly ConcurrentDictionary DelegateDic = new(); [Inject] public IHttpClientFactory ClientFactory { get; set; } = null!; + [Inject] public IObjectStorage ObjectStorage { get; set; } = null!; public virtual Task InParamRestructure(ForwardCoreContext context) { @@ -90,7 +91,7 @@ public class DefaultForwardFlow : IForwardFlow, ITransientDependency if (tokens.Count == 0) tokens = response.SelectTokens($"{pdfLabelConfig.Path}").ToList(); if (tokens.Count == 0) return mappedRes; var token = tokens.First(); - UploadResultDto oss = null!; + ObjectStorageSaveResult? oss = null; switch (Enum.Parse(pdfLabelConfig.Value)) { case LabelType.Url: @@ -98,14 +99,14 @@ public class DefaultForwardFlow : IForwardFlow, ITransientDependency string url = token.Value()!; using var r = await ClientFactory.CreateClient().GetAsync(url); await using var stream = await r.Content.ReadAsStreamAsync(); - oss = OSSHelper.Upload(stream, Guid.NewGuid().ToString("D") + ".pdf"); + oss = await SaveObjectAsync(stream, Guid.NewGuid().ToString("D") + ".pdf"); break; } case LabelType.Base64: byte[] bytes = Convert.FromBase64String(token.ToString()); if (bytes.Length > 0) { - oss = OSSHelper.Upload(new MemoryStream(bytes), Guid.NewGuid().ToString("D") + ".pdf"); + oss = await SaveObjectAsync(new MemoryStream(bytes), Guid.NewGuid().ToString("D") + ".pdf"); } break; @@ -113,8 +114,11 @@ public class DefaultForwardFlow : IForwardFlow, ITransientDependency throw new ArgumentOutOfRangeException(); } - mappedRes["oss_key"] = oss.Key; - mappedRes["oss_url"] = oss.TempUrl; + if (oss != null) + { + mappedRes["oss_key"] = oss.ObjectKey; + mappedRes["oss_url"] = oss.Url; + } } #endregion @@ -379,19 +383,19 @@ public class DefaultForwardFlow : IForwardFlow, ITransientDependency var base64 = await response.Content.ReadAsStringAsync(); byte[] pdfBytes = Convert.FromBase64String(base64); using MemoryStream stream = new MemoryStream(pdfBytes); - var ans = OSSHelper.Upload(stream, Guid.NewGuid().ToString("D") + ".pdf"); - return JsonConvert.SerializeObject(ans); + var ans = await SaveObjectAsync(stream, Guid.NewGuid().ToString("D") + ".pdf"); + return SerializeObjectStorageResult(ans); } // 流 if (context.TargetInterface.ResponseContentType is ResponseContentType.csv or ResponseContentType.pdf) { - // 上传oss + // 上传对象存储 string suffix = context.TargetInterface.ResponseContentType == ResponseContentType.csv ? "csv" : "pdf"; await using var stream = await response.Content.ReadAsStreamAsync(); - var ans = OSSHelper.Upload(stream, Guid.NewGuid().ToString("D") + "." + suffix); - return JsonConvert.SerializeObject(ans); + var ans = await SaveObjectAsync(stream, Guid.NewGuid().ToString("D") + "." + suffix); + return SerializeObjectStorageResult(ans); } if (response.Content.Headers.ContentType?.CharSet?.ToUpper() == "GBK") @@ -697,4 +701,27 @@ public class DefaultForwardFlow : IForwardFlow, ITransientDependency @delegate.DynamicInvoke(context); } } + + private Task SaveObjectAsync(Stream content, string objectKey) + { + return ObjectStorage.SaveAsync(new ObjectStorageSaveArgs + { + ObjectKey = objectKey, + Content = content, + Overwrite = true + }); + } + + /// + /// 序列化为历史字段名 Key / TempUrl,供出参映射读取。 + /// + private static string SerializeObjectStorageResult(ObjectStorageSaveResult result) + { + return JsonConvert.SerializeObject(new + { + Key = result.ObjectKey, + TempUrl = result.Url, + IsSuccess = true + }); + } } diff --git a/src/InterfaceForward.Application/InterfaceForward.Application.csproj b/src/InterfaceForward.Application/InterfaceForward.Application.csproj index bf388ff..54aff0c 100644 --- a/src/InterfaceForward.Application/InterfaceForward.Application.csproj +++ b/src/InterfaceForward.Application/InterfaceForward.Application.csproj @@ -1,6 +1,6 @@  - + net8.0 @@ -8,20 +8,20 @@ - - - + + + - + diff --git a/src/InterfaceForward.Application/InterfaceForwardApplicationModule.cs b/src/InterfaceForward.Application/InterfaceForwardApplicationModule.cs index a4d3809..64b433b 100644 --- a/src/InterfaceForward.Application/InterfaceForwardApplicationModule.cs +++ b/src/InterfaceForward.Application/InterfaceForwardApplicationModule.cs @@ -3,6 +3,7 @@ using Fake.AspNetCore; using Fake.EventBus.RabbitMQ; using Fake.Modularity; using Fake.ObjectMapping.AutoMapper; +using Fake.ObjectStorage; using Fake.RabbitMQ; using FreeRedis; using InterfaceForward.Application.Contracts; @@ -21,6 +22,7 @@ namespace InterfaceForward.Application; [DependsOn(typeof(FakeAspNetCoreModule))] [DependsOn(typeof(FakeObjectMappingAutoMapperModule))] [DependsOn(typeof(FakeEventBusRabbitMqModule))] +[DependsOn(typeof(FakeObjectStorageTencentCosModule))] [DependsOn(typeof(InterfaceForwardRepositoriesModule))] [DependsOn(typeof(InterfaceForwardDomainSharedModule))] public class InterfaceForwardApplicationModule:FakeModule diff --git a/src/InterfaceForward.Application/OSS/OSSHelper.cs b/src/InterfaceForward.Application/OSS/OSSHelper.cs deleted file mode 100644 index a762150..0000000 --- a/src/InterfaceForward.Application/OSS/OSSHelper.cs +++ /dev/null @@ -1,263 +0,0 @@ -using System.Security.Cryptography; -using Aliyun.OSS; - -namespace InterfaceForward.Application.OSS -{ - /// - /// 阿里云OSS帮助类 - /// - public static class OSSHelper - { - #region 初始化 - private static OSSOptions _OSSOptions; - internal static void Init(OSSOptions OSSOptions) - { - _OSSOptions = OSSOptions; - } - #endregion - - #region 获取client - private static OssClient GetClient() - { - string errorMessage = null; - if (string.IsNullOrEmpty(_OSSOptions?.Endpoint)) - { - errorMessage += $"未找到核心配置参数【{nameof(_OSSOptions.Endpoint)}】!"; - } - - if (string.IsNullOrEmpty(_OSSOptions?.AccessKeyId)) - { - errorMessage += $"未找到核心配置参数【{nameof(_OSSOptions.AccessKeyId)}】!"; - } - if (string.IsNullOrEmpty(_OSSOptions?.AccessKeySecret)) - { - errorMessage += $"未找到核心配置参数【{nameof(_OSSOptions.AccessKeySecret)}】!"; - } - - if (!string.IsNullOrEmpty(errorMessage)) - { - throw new Exception(errorMessage); - } - var client = new OssClient(_OSSOptions.Endpoint, _OSSOptions.AccessKeyId, _OSSOptions.AccessKeySecret); - return client; - } - #endregion - - #region 上传 - - /// - /// 上传文件 - /// - /// 文档流 - /// 文件名称 - /// 是否使用自定义ossKey,如果为true,会以fileName为key - /// - public static UploadResultDto Upload(Stream data, string fileName = "", bool usCustomKey = false, bool isPublicBucket = false) - { - if (data == null || data.Length == 0) - return new UploadResultDto(false); - - var result = Private_Upload(fileName, data, usCustomKey, isPublicBucket); - return result; - } - - private static UploadResultDto Private_Upload(string fileName, Stream data, bool usCustomKey = false, bool isPublicBucket = false) - { - var client = GetClient(); - - var ossKey = ""; - if (usCustomKey) - { - ossKey = fileName; - } - else - { - var md5 = ComputeDataMd5(data).ToUpper();// 计算文件MD5值 - var ext = Path.GetExtension(fileName); - ossKey = $"{md5}{ext}"; - } - - - // 文件已经存在直接返回 - var existingObject = client.DoesObjectExist(_OSSOptions.BucketName, ossKey); - if (existingObject) - { - var res = new UploadResultDto(true); - res.TempUrl = ossKey.GetUrlByKey(isPublicBucket); - res.Key = ossKey; - res.FileName = fileName; - - return res; - } - - // 上传文件 - bool putResult = false; - - using (var pubObj = client.PutObject(_OSSOptions.BucketName, ossKey, data)) - { - putResult = pubObj.HttpStatusCode == System.Net.HttpStatusCode.OK; - } - - - if (putResult)//上传成功 - { - var result = new UploadResultDto(true); - result.FileName = fileName; - result.TempUrl = ossKey.GetUrlByKey(isPublicBucket); - result.Key = ossKey; - return result; - } - else - { - // 上传失败 - var result = new UploadResultDto(false); - result.FileName = fileName; - return result; - } - } - public static string ComputeDataMd5(Stream data) - { - using (var md5 = MD5.Create()) - { - var hash = BitConverter.ToString(md5.ComputeHash(data)).Replace("-", "").Replace("=", "").ToLower(); - data.Position = 0; - return hash; - } - } - #endregion - - #region 下载 - - /// - /// 通过Key获取文件的URL - 同步 - /// - /// Key - /// 下载Url - public static string GetUrlByKey(this string key, bool isPublicBucket = false) - { - if (string.IsNullOrEmpty(key)) - { - throw new Exception($"指定的key不能为空"); - } - - try - { - var client = GetClient(); - Uri url = client.GeneratePresignedUri(_OSSOptions.BucketName, key); - - if (isPublicBucket) - { - var urlStr = url?.GetLeftPart(UriPartial.Path); - return (_OSSOptions.ReturnHttpsUrl && urlStr?.ToString()?.StartsWith("http://") == true) ? "https://" + urlStr.ToString().Substring(7) : urlStr.ToString(); - } - - - return (_OSSOptions.ReturnHttpsUrl && url?.ToString()?.StartsWith("http://") == true) ? "https://" + url.ToString().Substring(7) : url.ToString(); - } - catch (Exception ex) - { - throw new Exception(ex.Message); - } - } - - - - /// - /// 根据一组key返回url - /// - /// - /// - /// - public static Dictionary GetUrlByKeys(params string[] keys) - { - var resDic = new Dictionary(); - if (keys == null || keys.Count() <= 0) - return resDic; - - - var returnHttps = _OSSOptions.ReturnHttpsUrl; - var client = GetClient(); - - foreach (var item in keys) - { - if (resDic.ContainsKey(item)) - continue; - - Uri url = client.GeneratePresignedUri(_OSSOptions.BucketName, item); - if (url == null) - continue; - - resDic.Add(item, returnHttps ? url.ToString().Replace("http", "https") : url.ToString()); - } - return resDic; - } - - - /// - /// 通过Key获取文件流 - /// - /// Key - /// - public static Stream GetStreamByKey(this string key) - { - if (string.IsNullOrEmpty(key)) - { - throw new Exception($"指定的key不能为空"); - } - - - try - { - var client = GetClient(); - var obj = client.GetObject(_OSSOptions.BucketName, key); - if (obj != null) - { - return obj.Content; - } - else - { - throw new Exception($"文件获取异常"); - } - } - catch (Exception ex) - { - throw new Exception(ex.Message); - } - } - #endregion - - #region 删除 - /// - /// 删除文件 - /// - /// - /// - public static DeleteObjectsResult DeleteObjectsByKeys(params string[] keys) - { - if (keys == null || keys.Length <= 0) - return default(DeleteObjectsResult); - - var client = GetClient(); - var request = new DeleteObjectsRequest(_OSSOptions.BucketName, keys); - - return client.DeleteObjects(request); - } - #endregion - - #region 查看数据信息 - /// - /// 查看文档信息 - /// - /// - /// - public static ObjectMetadata GetObjectMetadata(string ossKey) - { - var client = GetClient().GetObjectMetadata(_OSSOptions.BucketName, ossKey); - return client; - } - #endregion - - - - } -} diff --git a/src/InterfaceForward.Application/OSS/OSSOptions.cs b/src/InterfaceForward.Application/OSS/OSSOptions.cs deleted file mode 100644 index 647d977..0000000 --- a/src/InterfaceForward.Application/OSS/OSSOptions.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace InterfaceForward.Application.OSS; - -public class OSSOptions -{ - - /// - /// 秘钥Id(类似于公钥) - /// - public string AccessKeyId { get; set; } - - /// - /// 访问秘钥 - /// - public string AccessKeySecret { get; set; } - - /// - /// OSS端点 - /// - public string Endpoint { get; set; } - - /// - /// 存储空间名称 - /// 注意,如果有多个BucketName的话,可以直接使用代码的方式设置这个值 - /// - public string BucketName { get; set; } - - /// - /// 返回https的url,默认false返回http - /// 阿里OSS默认支持http,https,不需要额外配置 - /// - public bool ReturnHttpsUrl { get; set; } -} \ No newline at end of file diff --git a/src/InterfaceForward.Application/OSS/UploadResultDto.cs b/src/InterfaceForward.Application/OSS/UploadResultDto.cs deleted file mode 100644 index 6873769..0000000 --- a/src/InterfaceForward.Application/OSS/UploadResultDto.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace InterfaceForward.Application.OSS; - -public class UploadResultDto -{ - /// - /// 标识本次操作是否成功 - /// - public bool IsSuccess { get; set; } - - /// - /// 临时URL,过期时间15min - /// - public string TempUrl { get; set; } - - /// - /// 文件唯一标识Key,通常保存在数据 - /// 后期所有的文件操作都是通过这个Key - /// - public string Key { get; set; } - - /// - /// 文件原始名称 - /// - public string FileName { get; set; } - - public UploadResultDto(bool isSuccess) - { - IsSuccess = isSuccess; - } -} \ No newline at end of file diff --git a/src/InterfaceForward.Application/Services/App/AppsService.cs b/src/InterfaceForward.Application/Services/App/AppsService.cs index 93136ae..383835e 100644 --- a/src/InterfaceForward.Application/Services/App/AppsService.cs +++ b/src/InterfaceForward.Application/Services/App/AppsService.cs @@ -1,9 +1,9 @@ using System.ComponentModel.DataAnnotations; using System.Text; +using Fake.ObjectStorage; using Fake.UnitOfWork; using InterfaceForward.Application.Contracts.Dtos.App; using InterfaceForward.Application.Helpers; -using InterfaceForward.Application.OSS; using InterfaceForward.Repositories; using InterfaceForward.Repositories.App.Entitys; using InterfaceForward.Repositories.App.Services; @@ -24,6 +24,7 @@ public class AppsService : ApplicationService private readonly IAppRepository _appRepository; private readonly IAppScopeRepository _appScopeRepository; private readonly IBasicRepository _interfaceRepository; + private readonly IObjectStorage _objectStorage; /// /// @@ -31,11 +32,13 @@ public class AppsService : ApplicationService public AppsService( IAppRepository appRepository , IAppScopeRepository appScopeRepository - , IBasicRepository interfaceRepository) + , IBasicRepository interfaceRepository + , IObjectStorage objectStorage) { _appRepository = appRepository; _appScopeRepository = appScopeRepository; _interfaceRepository = interfaceRepository; + _objectStorage = objectStorage; } #region Get @@ -53,13 +56,26 @@ public class AppsService : ApplicationService .OrderByDescending(a => a.UpdateTime) .ToPagedListAsync(input.PageIndex, input.PageSize); - // var logoKeys = res.Items - // .Where(x => !x.LogoKey.IsNullOrWhiteSpace()) - // .Select(x => x.LogoKey).ToArray(); - // var urlDic = OSSHelper.GetUrlByKeys(logoKeys); + var logoKeys = res.Items + .Where(x => !x.LogoKey.IsNullOrWhiteSpace()) + .Select(x => x.LogoKey) + .Distinct() + .ToArray(); - // foreach (var url in urlDic) - // res.Items.First(x => x.LogoKey == url.Key).LogoUrl = url.Value; + if (logoKeys.Length > 0) + { + var urlDic = new Dictionary(); + foreach (var logoKey in logoKeys) + { + urlDic[logoKey] = await _objectStorage.GetUrlAsync(logoKey); + } + + foreach (var item in res.Items.Where(x => !x.LogoKey.IsNullOrWhiteSpace())) + { + if (urlDic.TryGetValue(item.LogoKey, out var logoUrl)) + item.LogoUrl = logoUrl; + } + } return res; } @@ -135,7 +151,9 @@ public class AppsService : ApplicationService .SingleAsync(x => x.Id == id); _ = res ?? throw new BusinessException("在指定的编号下找不到数据"); - // if (res.LogoKey != null) res.LogoUrl = OSSHelper.GetUrlByKey(res.LogoKey); + if (!res.LogoKey.IsNullOrWhiteSpace()) + res.LogoUrl = await _objectStorage.GetUrlAsync(res.LogoKey); + res.AppScope = await _appScopeRepository.AsQueryable() .Where(x => x.AppId == id) .Select(x => new AppScopeOutput { Id = x.Id.SelectAll() })